mirror of
https://github.com/DarthAffe/ScreenCapture.NET.git
synced 2025-12-12 13:28:35 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 056cb77772 | |||
| af9f75c63e | |||
| 44a669c827 | |||
| f314a4128b | |||
| e9eb490607 | |||
| fd228af170 | |||
| 122327532b | |||
| d74f3a6b21 | |||
| 4a794409c4 | |||
| c30865aec6 | |||
| ae6cf5c7c2 | |||
|
|
a53623e136 | ||
|
|
183f02da94 | ||
|
|
47055de4f2 | ||
| f5881432fe | |||
| aa643f1fe3 | |||
| 0aea8c675b | |||
| 762e2fefe0 | |||
| f2801667a6 | |||
| 2447747385 | |||
| b6132a4ee3 | |||
| 7dfb0e9cfd | |||
| 797a2bb7c1 | |||
| c9ef36176d | |||
| fb4876e2f6 | |||
| f4ef78fe01 | |||
| a89533aea7 | |||
| 5eb8b5636e | |||
| d5e17f7370 | |||
| b8b6423005 | |||
| f04d270d8e | |||
| 9d8c45da39 | |||
| 7c768e08ae | |||
| 9cc6cfb653 | |||
| d0fc30998a | |||
| c2aed2b186 | |||
| 5e6acc6055 | |||
| 8e79b2c029 | |||
| 0675b6538a | |||
| e75601ab5a | |||
| 610d9da25d | |||
| b44703b4d0 | |||
| 57462e9717 | |||
| bcb92eb422 | |||
| a1a43ddcfc | |||
| 07a175e552 | |||
| 94471f08b4 | |||
| 2667f84c42 | |||
| 6919cf7b60 | |||
| ec57728521 | |||
| adacf41d07 | |||
| f55fa6a701 | |||
| 18e562508e | |||
| 946dafe649 | |||
| 52d37b996e | |||
| db1e37f1d9 | |||
| ba5233be6f | |||
| 1782c83415 | |||
| 9f9f153da5 | |||
| 8ce7db15e8 | |||
| 9066366ed0 | |||
| d5d1584cf2 | |||
| bd0d49f774 |
29
.github/workflows/release.yml
vendored
Normal file
29
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: ScreenCapture.NET-Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: |
|
||||
8.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --no-restore --configuration Release
|
||||
- name: Test
|
||||
run: dotnet test --no-build --verbosity normal --configuration Release
|
||||
- name: Nuget Push
|
||||
run: dotnet nuget push **\*.nupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json
|
||||
- name: Symbols Push
|
||||
run: dotnet nuget push **\*.snupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json
|
||||
108
README.md
108
README.md
@ -4,6 +4,14 @@
|
||||
[](https://github.com/DarthAffe/ScreenCapture.NET/blob/master/LICENSE)
|
||||
[](https://github.com/DarthAffe/ScreenCapture.NET/stargazers)
|
||||
|
||||
## NuGet-Packages:
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| [ScreenCapture.NET](https://www.nuget.org/packages/ScreenCapture.NET)| The core-package required to use ScreenCapture.NET captures or write your own. |
|
||||
| [ScreenCapture.NET.DX11](https://www.nuget.org/packages/ScreenCapture.NET.DX11) | DirectX 11 based capturing. Fast and supports the whole set of features. **This should always be used if possible!** *Note: This might not work on windows hybrid systems (check [the docs](https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/error-when-dda-capable-app-is-against-gpu) and [this issue](https://github.com/DarthAffe/ScreenCapture.NET/issues/39))*|
|
||||
| [ScreenCapture.NET.DX9](https://www.nuget.org/packages/ScreenCapture.NET.DX9) | DirectX 9 based capturing. Slower then DX 11 and does not support rotated screens and GPU-accelerated downscaling. Only useful if the DX11 package can't be used for some reason. |
|
||||
| [ScreenCapture.NET.X11](https://www.nuget.org/packages/ScreenCapture.NET.X11) | libX11 based capturing for the X-Window-System. Currently the only way to use ScreenCapture.NET on linux. Quite slow and can easily break depending on the X-Server config. Works on my machine, but it's not really a high proprity to support at the moment. Does not support rotated screens and GPU-accelerated downscaling. |
|
||||
|
||||
## Usage
|
||||
```csharp
|
||||
// Create a screen-capture service
|
||||
@ -20,48 +28,82 @@ IScreenCapture screenCapture = screenCaptureService.GetScreenCapture(displays.Fi
|
||||
|
||||
// Register the regions you want to capture on the screen
|
||||
// Capture the whole screen
|
||||
CaptureZone fullscreen = screenCapture.RegisterCaptureZone(0, 0, screenCapture.Display.Width, screenCapture.Display.Height);
|
||||
ICaptureZone fullscreen = screenCapture.RegisterCaptureZone(0, 0, screenCapture.Display.Width, screenCapture.Display.Height);
|
||||
// Capture a 100x100 region at the top left and scale it down to 50x50
|
||||
CaptureZone topLeft = screenCapture.RegisterCaptureZone(0, 0, 100, 100, downscaleLevel: 1);
|
||||
ICaptureZone topLeft = screenCapture.RegisterCaptureZone(0, 0, 100, 100, downscaleLevel: 1);
|
||||
|
||||
// Capture the screen
|
||||
// This should be done in a loop on a seperate thread as CaptureScreen blocks if the screen is not updated (still image).
|
||||
// This should be done in a loop on a separate thread as CaptureScreen blocks if the screen is not updated (still image).
|
||||
screenCapture.CaptureScreen();
|
||||
|
||||
// Do something with the captured image - e.g. access all pixels (same could be done with topLeft)
|
||||
// Locking is not neccessary in that case as we're capturing in the same thread,
|
||||
// but when using a threaded-approach (which is recommended) it prevents potential tearing of the data in the buffer.
|
||||
lock (fullscreen.Buffer)
|
||||
|
||||
//Lock the zone to access the data. Remember to dispose the returned disposable to unlock again.
|
||||
using (fullscreen.Lock())
|
||||
{
|
||||
// Stride is the width in bytes of a row in the buffer (width in pixel * bytes per pixel)
|
||||
int stride = fullscreen.Stride;
|
||||
// You have multiple options now:
|
||||
// 1. Access the raw byte-data
|
||||
ReadOnlySpan<byte> rawData = fullscreen.RawBuffer;
|
||||
|
||||
Span<byte> data = new(fullscreen.Buffer);
|
||||
// 2. Use the provided abstraction to access pixels without having to care about low-level byte handling
|
||||
// Get the image captured for the zone
|
||||
IImage image = fullscreen.Image;
|
||||
|
||||
// Iterate all rows of the image
|
||||
for (int y = 0; y < fullscreen.Height; y++)
|
||||
{
|
||||
// Select the actual data of the row
|
||||
Span<byte> row = data.Slice(y * stride, stride);
|
||||
// Iterate all pixels of the image
|
||||
foreach (IColor color in image)
|
||||
Console.WriteLine($"A: {color.A}, R: {color.R}, G: {color.G}, B: {color.B}");
|
||||
|
||||
// Iterate all pixels
|
||||
for (int x = 0; x < row.Length; x += fullscreen.BytesPerPixel)
|
||||
{
|
||||
// Data is in BGRA format for the DX11ScreenCapture
|
||||
byte b = row[x];
|
||||
byte g = row[x + 1];
|
||||
byte r = row[x + 2];
|
||||
byte a = row[x + 3];
|
||||
}
|
||||
}
|
||||
// Get the pixel at location (x = 10, y = 20)
|
||||
IColor imageColorExample = image[10, 20];
|
||||
|
||||
// Get the first row
|
||||
IImageRow row = image.Rows[0];
|
||||
// Get the 10th pixel of the row
|
||||
IColor rowColorExample = row[10];
|
||||
|
||||
// Get the first column
|
||||
IImageColumn column = image.Columns[0];
|
||||
// Get the 10th pixel of the column
|
||||
IColor columnColorExample = column[10];
|
||||
|
||||
// Cuts a rectangle out of the original image (x = 100, y = 150, width = 400, height = 300)
|
||||
IImage subImage = image[100, 150, 400, 300];
|
||||
|
||||
// All of the things above (rows, columns, sub-images) do NOT allocate new memory so they are fast and memory efficient, but for that reason don't provide raw byte access.
|
||||
}
|
||||
```
|
||||
|
||||
If you know which Capture-provider you're using it performs a bit better to not use the abstraction but a more low-level approach instead.
|
||||
This is the same example as above but without using the interfaces:
|
||||
```csharp
|
||||
DX11ScreenCaptureService screenCaptureService = new DX11ScreenCaptureService();
|
||||
IEnumerable<GraphicsCard> graphicsCards = screenCaptureService.GetGraphicsCards();
|
||||
IEnumerable<Display> displays = screenCaptureService.GetDisplays(graphicsCards.First());
|
||||
DX11ScreenCapture screenCapture = screenCaptureService.GetScreenCapture(displays.First());
|
||||
|
||||
CaptureZone<ColorBGRA> fullscreen = screenCapture.RegisterCaptureZone(0, 0, screenCapture.Display.Width, screenCapture.Display.Height);
|
||||
CaptureZone<ColorBGRA> topLeft = screenCapture.RegisterCaptureZone(0, 0, 100, 100, downscaleLevel: 1);
|
||||
|
||||
screenCapture.CaptureScreen();
|
||||
|
||||
using (fullscreen.Lock())
|
||||
{
|
||||
IImage<ColorBGRA> image = fullscreen.Image;
|
||||
|
||||
// You can also get a ref image which has a slight performance benefit in some cases
|
||||
// RefImage<ColorBGRA> refImage = image.AsRefImage();
|
||||
|
||||
foreach (ColorBGRA color in image)
|
||||
Console.WriteLine($"A: {color.A}, R: {color.R}, G: {color.G}, B: {color.B}");
|
||||
|
||||
ColorBGRA imageColorExample = image[10, 20];
|
||||
|
||||
ImageRow<ColorBGRA> row = image.Rows[0];
|
||||
ColorBGRA rowColorExample = row[10];
|
||||
|
||||
ImageColumn<ColorBGRA> column = image.Columns[0];
|
||||
ColorBGRA columnColorExample = column[10];
|
||||
|
||||
RefImage<ColorBGRA> subImage = image[100, 150, 400, 300];
|
||||
}
|
||||
|
||||
// Move the top left zone more towards the center
|
||||
// Using the Update-method allows to move the zone without having to allocate
|
||||
// new buffers and textures which yields a good performance gain if done at high framerates.
|
||||
screenCapture.UpdateCaptureZone(topLeft, x: 100, y: 200);
|
||||
|
||||
// Note that resizing the zone is also possible but currently reinitializes the zone
|
||||
// -> no performance gain compared to removing and readding the zone.
|
||||
screenCapture.UpdateCaptureZone(topLeft, width: 20, height: 20);
|
||||
```
|
||||
|
||||
516
ScreenCapture.NET.DX11/DX11ScreenCapture.cs
Normal file
516
ScreenCapture.NET.DX11/DX11ScreenCapture.cs
Normal file
@ -0,0 +1,516 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using HPPH;
|
||||
using SharpGen.Runtime;
|
||||
using Vortice.Direct3D;
|
||||
using Vortice.Direct3D11;
|
||||
using Vortice.DXGI;
|
||||
using Vortice.Mathematics;
|
||||
using MapFlags = Vortice.Direct3D11.MapFlags;
|
||||
using ResultCode = Vortice.DXGI.ResultCode;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a ScreenCapture using DirectX 11 desktop duplicaton.
|
||||
/// https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public sealed class DX11ScreenCapture : AbstractScreenCapture<ColorBGRA>
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private static readonly FeatureLevel[] FEATURE_LEVELS =
|
||||
[
|
||||
FeatureLevel.Level_11_1,
|
||||
FeatureLevel.Level_11_0,
|
||||
FeatureLevel.Level_10_1,
|
||||
FeatureLevel.Level_10_0
|
||||
];
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly object _captureLock = new();
|
||||
|
||||
private readonly bool _useNewDuplicationAdapter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in ms used for screen-capturing. (default 1000ms)
|
||||
/// This is used in <see cref="PerformScreenCapture"/> https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-acquirenextframe
|
||||
/// </summary>
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public int Timeout { get; set; } = 1000;
|
||||
|
||||
private readonly IDXGIFactory1 _factory;
|
||||
|
||||
private IDXGIOutput? _output;
|
||||
private IDXGIOutputDuplication? _duplicatedOutput;
|
||||
private ID3D11Device? _device;
|
||||
private ID3D11DeviceContext? _context;
|
||||
private ID3D11Texture2D? _captureTexture;
|
||||
|
||||
private readonly Dictionary<CaptureZone<ColorBGRA>, ZoneTextures> _textures = [];
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DX11ScreenCapture"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that setting useNewDuplicationAdapter to true requires to call <c>DPIAwareness.Initalize();</c> and prevents the capture from running in a WPF-thread.
|
||||
/// </remarks>
|
||||
/// <param name="factory">The <see cref="IDXGIFactory1"/> used to create underlying objects.</param>
|
||||
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
|
||||
/// <param name="useNewDuplicationAdapter">Indicates if the DuplicateOutput1 interface should be used instead of the older DuplicateOutput. Currently there's no real use in setting this to true.</param>
|
||||
internal DX11ScreenCapture(IDXGIFactory1 factory, Display display, bool useNewDuplicationAdapter = false)
|
||||
: base(display)
|
||||
{
|
||||
this._factory = factory;
|
||||
this._useNewDuplicationAdapter = useNewDuplicationAdapter;
|
||||
|
||||
Restart();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool PerformScreenCapture()
|
||||
{
|
||||
bool result = false;
|
||||
lock (_captureLock)
|
||||
{
|
||||
if ((_context == null) || (_duplicatedOutput == null) || (_captureTexture == null))
|
||||
{
|
||||
Restart();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IDXGIResource? screenResource = null;
|
||||
try
|
||||
{
|
||||
_duplicatedOutput.AcquireNextFrame(Timeout, out OutduplFrameInfo duplicateFrameInformation, out screenResource).CheckError();
|
||||
if ((screenResource == null) || (duplicateFrameInformation.LastPresentTime == 0)) return false;
|
||||
|
||||
using ID3D11Texture2D screenTexture = screenResource.QueryInterface<ID3D11Texture2D>();
|
||||
_context.CopySubresourceRegion(_captureTexture, 0, 0, 0, 0, screenTexture, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
screenResource?.Dispose();
|
||||
_duplicatedOutput?.ReleaseFrame();
|
||||
}
|
||||
catch { /**/ }
|
||||
}
|
||||
|
||||
result = true;
|
||||
}
|
||||
catch (SharpGenException dxException)
|
||||
{
|
||||
if ((dxException.ResultCode == ResultCode.AccessLost)
|
||||
|| (dxException.ResultCode == ResultCode.AccessDenied)
|
||||
|| (dxException.ResultCode == ResultCode.InvalidCall))
|
||||
{
|
||||
try
|
||||
{
|
||||
Restart();
|
||||
}
|
||||
catch { Thread.Sleep(100); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
if (_context == null) return;
|
||||
|
||||
lock (_textures)
|
||||
{
|
||||
if (!_textures.TryGetValue(captureZone, out ZoneTextures? textures)) return;
|
||||
|
||||
if (textures.ScalingTexture != null)
|
||||
{
|
||||
_context.CopySubresourceRegion(textures.ScalingTexture, 0, 0, 0, 0, _captureTexture, 0,
|
||||
new Box(textures.X, textures.Y, 0,
|
||||
textures.X + textures.UnscaledWidth,
|
||||
textures.Y + textures.UnscaledHeight, 1));
|
||||
_context.GenerateMips(textures.ScalingTextureView);
|
||||
_context.CopySubresourceRegion(textures.StagingTexture, 0, 0, 0, 0, textures.ScalingTexture, captureZone.DownscaleLevel);
|
||||
}
|
||||
else
|
||||
_context.CopySubresourceRegion(textures.StagingTexture, 0, 0, 0, 0, _captureTexture, 0,
|
||||
new Box(textures.X, textures.Y, 0,
|
||||
textures.X + textures.UnscaledWidth,
|
||||
textures.Y + textures.UnscaledHeight, 1));
|
||||
|
||||
MappedSubresource mapSource = _context.Map(textures.StagingTexture, 0, MapMode.Read, MapFlags.None);
|
||||
|
||||
using IDisposable @lock = captureZone.Lock();
|
||||
{
|
||||
ReadOnlySpan<byte> source = mapSource.AsSpan(mapSource.RowPitch * textures.Height);
|
||||
switch (Display.Rotation)
|
||||
{
|
||||
case Rotation.Rotation90:
|
||||
CopyRotate90(source, mapSource.RowPitch, captureZone, buffer);
|
||||
break;
|
||||
|
||||
case Rotation.Rotation180:
|
||||
CopyRotate180(source, mapSource.RowPitch, captureZone, buffer);
|
||||
break;
|
||||
|
||||
case Rotation.Rotation270:
|
||||
CopyRotate270(source, mapSource.RowPitch, captureZone, buffer);
|
||||
break;
|
||||
|
||||
default:
|
||||
CopyRotate0(source, mapSource.RowPitch, captureZone, buffer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_context.Unmap(textures.StagingTexture, 0);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate0(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
int height = captureZone.Height;
|
||||
int stride = captureZone.Stride;
|
||||
Span<byte> target = buffer;
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
int sourceOffset = y * sourceStride;
|
||||
int targetOffset = y * stride;
|
||||
|
||||
source.Slice(sourceOffset, stride).CopyTo(target.Slice(targetOffset, stride));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate90(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel;
|
||||
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
ReadOnlySpan<ColorBGRA> src = MemoryMarshal.Cast<byte, ColorBGRA>(source.Slice(x * sourceStride, usedBytesPerLine));
|
||||
for (int y = 0; y < src.Length; y++)
|
||||
target[(y * width) + (width - x - 1)] = src[y];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate180(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
int bpp = captureZone.ColorFormat.BytesPerPixel;
|
||||
int usedBytesPerLine = width * bpp;
|
||||
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
ReadOnlySpan<ColorBGRA> src = MemoryMarshal.Cast<byte, ColorBGRA>(source.Slice(y * sourceStride, usedBytesPerLine));
|
||||
for (int x = 0; x < src.Length; x++)
|
||||
target[((height - y - 1) * width) + (width - x - 1)] = src[x];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate270(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel;
|
||||
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
ReadOnlySpan<ColorBGRA> src = MemoryMarshal.Cast<byte, ColorBGRA>(source.Slice(x * sourceStride, usedBytesPerLine));
|
||||
for (int y = 0; y < src.Length; y++)
|
||||
target[((height - y - 1) * width) + x] = src[y];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override CaptureZone<ColorBGRA> RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0)
|
||||
{
|
||||
CaptureZone<ColorBGRA> captureZone = base.RegisterCaptureZone(x, y, width, height, downscaleLevel);
|
||||
|
||||
lock (_textures)
|
||||
InitializeCaptureZone(captureZone);
|
||||
|
||||
return captureZone;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool UnregisterCaptureZone(CaptureZone<ColorBGRA> captureZone)
|
||||
{
|
||||
if (!base.UnregisterCaptureZone(captureZone)) return false;
|
||||
|
||||
lock (_textures)
|
||||
{
|
||||
if (_textures.TryGetValue(captureZone, out ZoneTextures? textures))
|
||||
{
|
||||
textures.Dispose();
|
||||
_textures.Remove(captureZone);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void UpdateCaptureZone(CaptureZone<ColorBGRA> captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null)
|
||||
{
|
||||
base.UpdateCaptureZone(captureZone, x, y, width, height, downscaleLevel);
|
||||
|
||||
//TODO DarthAffe 01.05.2022: For now just reinitialize the zone in that case, but this could be optimized to only recreate the textures needed.
|
||||
if ((width != null) || (height != null) || (downscaleLevel != null))
|
||||
{
|
||||
lock (_textures)
|
||||
{
|
||||
if (_textures.TryGetValue(captureZone, out ZoneTextures? textures))
|
||||
{
|
||||
textures.Dispose();
|
||||
InitializeCaptureZone(captureZone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ValidateCaptureZoneAndThrow(int x, int y, int width, int height, int downscaleLevel)
|
||||
{
|
||||
if (_device == null) throw new ApplicationException("ScreenCapture isn't initialized.");
|
||||
|
||||
base.ValidateCaptureZoneAndThrow(x, y, width, height, downscaleLevel);
|
||||
}
|
||||
|
||||
private void InitializeCaptureZone(CaptureZone<ColorBGRA> captureZone)
|
||||
{
|
||||
int x;
|
||||
int y;
|
||||
int width;
|
||||
int height;
|
||||
int unscaledWidth;
|
||||
int unscaledHeight;
|
||||
|
||||
if (captureZone.Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
|
||||
{
|
||||
x = captureZone.Y;
|
||||
y = captureZone.X;
|
||||
width = captureZone.Height;
|
||||
height = captureZone.Width;
|
||||
unscaledWidth = captureZone.UnscaledHeight;
|
||||
unscaledHeight = captureZone.UnscaledWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = captureZone.X;
|
||||
y = captureZone.Y;
|
||||
width = captureZone.Width;
|
||||
height = captureZone.Height;
|
||||
unscaledWidth = captureZone.UnscaledWidth;
|
||||
unscaledHeight = captureZone.UnscaledHeight;
|
||||
}
|
||||
|
||||
Texture2DDescription stagingTextureDesc = new()
|
||||
{
|
||||
CPUAccessFlags = CpuAccessFlags.Read,
|
||||
BindFlags = BindFlags.None,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Width = width,
|
||||
Height = height,
|
||||
MiscFlags = ResourceOptionFlags.None,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Staging
|
||||
};
|
||||
ID3D11Texture2D stagingTexture = _device!.CreateTexture2D(stagingTextureDesc);
|
||||
|
||||
ID3D11Texture2D? scalingTexture = null;
|
||||
ID3D11ShaderResourceView? scalingTextureView = null;
|
||||
if (captureZone.DownscaleLevel > 0)
|
||||
{
|
||||
Texture2DDescription scalingTextureDesc = new()
|
||||
{
|
||||
CPUAccessFlags = CpuAccessFlags.None,
|
||||
BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Width = unscaledWidth,
|
||||
Height = unscaledHeight,
|
||||
MiscFlags = ResourceOptionFlags.GenerateMips,
|
||||
MipLevels = captureZone.DownscaleLevel + 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Default
|
||||
};
|
||||
scalingTexture = _device!.CreateTexture2D(scalingTextureDesc);
|
||||
scalingTextureView = _device.CreateShaderResourceView(scalingTexture);
|
||||
}
|
||||
|
||||
_textures[captureZone] = new ZoneTextures(x, y, width, height, unscaledWidth, unscaledHeight, stagingTexture, scalingTexture, scalingTextureView);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Restart()
|
||||
{
|
||||
base.Restart();
|
||||
|
||||
lock (_captureLock)
|
||||
lock (_textures)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (ZoneTextures textures in _textures.Values)
|
||||
textures.Dispose();
|
||||
_textures.Clear();
|
||||
|
||||
DisposeDX();
|
||||
|
||||
using IDXGIAdapter1 adapter = _factory.GetAdapter1(Display.GraphicsCard.Index) ?? throw new ApplicationException("Couldn't create DirectX-Adapter.");
|
||||
|
||||
D3D11.D3D11CreateDevice(adapter, DriverType.Unknown, DeviceCreationFlags.None, FEATURE_LEVELS, out _device).CheckError();
|
||||
_context = _device!.ImmediateContext;
|
||||
|
||||
_output = adapter.GetOutput(Display.Index) ?? throw new ApplicationException("Couldn't get DirectX-Output.");
|
||||
using IDXGIOutput5 output = _output.QueryInterface<IDXGIOutput5>();
|
||||
|
||||
int width = Display.Width;
|
||||
int height = Display.Height;
|
||||
if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
|
||||
(width, height) = (height, width);
|
||||
|
||||
Texture2DDescription captureTextureDesc = new()
|
||||
{
|
||||
CPUAccessFlags = CpuAccessFlags.None,
|
||||
BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Width = width,
|
||||
Height = height,
|
||||
MiscFlags = ResourceOptionFlags.None,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Default
|
||||
};
|
||||
_captureTexture = _device.CreateTexture2D(captureTextureDesc);
|
||||
|
||||
foreach (CaptureZone<ColorBGRA> captureZone in CaptureZones)
|
||||
InitializeCaptureZone(captureZone);
|
||||
|
||||
if (_useNewDuplicationAdapter)
|
||||
_duplicatedOutput = output.DuplicateOutput1(_device, new[] { Format.B8G8R8A8_UNorm }); // DarthAffe 27.02.2021: This prepares for the use of 10bit color depth
|
||||
else
|
||||
_duplicatedOutput = output.DuplicateOutput(_device);
|
||||
}
|
||||
catch { DisposeDX(); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
lock (_captureLock)
|
||||
{
|
||||
foreach (ZoneTextures textures in _textures.Values)
|
||||
textures.Dispose();
|
||||
_textures.Clear();
|
||||
|
||||
DisposeDX();
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeDX()
|
||||
{
|
||||
try
|
||||
{
|
||||
try { _duplicatedOutput?.Dispose(); } catch { /**/ }
|
||||
try { _output?.Dispose(); } catch { /**/ }
|
||||
try { _context?.Dispose(); } catch { /**/ }
|
||||
try { _device?.Dispose(); } catch { /**/ }
|
||||
try { _captureTexture?.Dispose(); } catch { /**/ }
|
||||
|
||||
_duplicatedOutput = null;
|
||||
_context = null;
|
||||
_captureTexture = null;
|
||||
}
|
||||
catch { /**/ }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class ZoneTextures : IDisposable
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public int UnscaledWidth { get; }
|
||||
public int UnscaledHeight { get; }
|
||||
|
||||
public ID3D11Texture2D StagingTexture { get; }
|
||||
public ID3D11Texture2D? ScalingTexture { get; }
|
||||
public ID3D11ShaderResourceView? ScalingTextureView { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public ZoneTextures(int x, int y, int width, int height, int unscaledWidth, int unscaledHeight,
|
||||
ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? scalingTextureView)
|
||||
{
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.UnscaledWidth = unscaledWidth;
|
||||
this.UnscaledHeight = unscaledHeight;
|
||||
this.StagingTexture = stagingTexture;
|
||||
this.ScalingTexture = scalingTexture;
|
||||
this.ScalingTextureView = scalingTextureView;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StagingTexture.Dispose();
|
||||
ScalingTexture?.Dispose();
|
||||
ScalingTextureView?.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -12,9 +12,10 @@ public class DX11ScreenCaptureService : IScreenCaptureService
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly IDXGIFactory1 _factory;
|
||||
|
||||
private readonly Dictionary<Display, DX11ScreenCapture> _screenCaptures = new();
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
@ -27,6 +28,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
|
||||
DXGI.CreateDXGIFactory1(out _factory!).CheckError();
|
||||
}
|
||||
|
||||
~DX11ScreenCaptureService() => Dispose();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
@ -34,6 +37,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<GraphicsCard> GetGraphicsCards()
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
int i = 0;
|
||||
while (_factory.EnumAdapters1(i, out IDXGIAdapter1 adapter).Success)
|
||||
{
|
||||
@ -46,6 +51,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Display> GetDisplays(GraphicsCard graphicsCard)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
using IDXGIAdapter1? adapter = _factory.GetAdapter1(graphicsCard.Index);
|
||||
|
||||
int i = 0;
|
||||
@ -59,7 +66,7 @@ public class DX11ScreenCaptureService : IScreenCaptureService
|
||||
}
|
||||
}
|
||||
|
||||
private Rotation GetRotation(ModeRotation rotation) => rotation switch
|
||||
private static Rotation GetRotation(ModeRotation rotation) => rotation switch
|
||||
{
|
||||
ModeRotation.Rotate90 => Rotation.Rotation90,
|
||||
ModeRotation.Rotate180 => Rotation.Rotation180,
|
||||
@ -68,8 +75,11 @@ public class DX11ScreenCaptureService : IScreenCaptureService
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public IScreenCapture GetScreenCapture(Display display)
|
||||
IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
|
||||
public DX11ScreenCapture GetScreenCapture(Display display)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
if (!_screenCaptures.TryGetValue(display, out DX11ScreenCapture? screenCapture))
|
||||
_screenCaptures.Add(display, screenCapture = new DX11ScreenCapture(_factory, display));
|
||||
return screenCapture;
|
||||
@ -78,13 +88,17 @@ public class DX11ScreenCaptureService : IScreenCaptureService
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
foreach (DX11ScreenCapture screenCapture in _screenCaptures.Values)
|
||||
screenCapture.Dispose();
|
||||
_screenCaptures.Clear();
|
||||
|
||||
_factory.Dispose();
|
||||
try { _factory.Dispose(); } catch { /**/ }
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
BIN
ScreenCapture.NET.DX11/Resources/icon.png
Normal file
BIN
ScreenCapture.NET.DX11/Resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 B |
72
ScreenCapture.NET.DX11/ScreenCapture.NET.DX11.csproj
Normal file
72
ScreenCapture.NET.DX11/ScreenCapture.NET.DX11.csproj
Normal file
@ -0,0 +1,72 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<Authors>Darth Affe</Authors>
|
||||
<Company>Wyrez</Company>
|
||||
<Language>en-US</Language>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Title>ScreenCapture.NET.DX11</Title>
|
||||
<AssemblyName>ScreenCapture.NET.DX11</AssemblyName>
|
||||
<AssemblyTitle>ScreenCapture.NET.DX11</AssemblyTitle>
|
||||
<PackageId>ScreenCapture.NET.DX11</PackageId>
|
||||
<RootNamespace>ScreenCapture.NET</RootNamespace>
|
||||
<Description>DirectX 11 based Screen-Capturing using the Desktop Duplication API</Description>
|
||||
<Summary>DirectX 11 based Screen-Capturing using the Desktop Duplication API</Summary>
|
||||
<Copyright>Copyright © Darth Affe 2024</Copyright>
|
||||
<PackageCopyright>Copyright © Darth Affe 2024</PackageCopyright>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/DarthAffe/ScreenCapture.NET</PackageProjectUrl>
|
||||
<PackageLicenseExpression>LGPL-2.1-only</PackageLicenseExpression>
|
||||
<RepositoryType>Github</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/DarthAffe/ScreenCapture.NET</RepositoryUrl>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
|
||||
<PackageReleaseNotes>
|
||||
</PackageReleaseNotes>
|
||||
|
||||
<Version>3.0.0</Version>
|
||||
<AssemblyVersion>3.0.0</AssemblyVersion>
|
||||
<FileVersion>3.0.0</FileVersion>
|
||||
|
||||
<OutputPath>..\bin\</OutputPath>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IncludeSource>True</IncludeSource>
|
||||
<IncludeSymbols>True</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<DefineConstants>$(DefineConstants);TRACE;DEBUG</DefineConstants>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>$(NoWarn);CS1591;CS1572;CS1573</NoWarn>
|
||||
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Resources\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Vortice.Direct3D11" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helper/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
184
ScreenCapture.NET.DX9/DX9ScreenCapture.cs
Normal file
184
ScreenCapture.NET.DX9/DX9ScreenCapture.cs
Normal file
@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using HPPH;
|
||||
using SharpGen.Runtime;
|
||||
using Vortice.Direct3D9;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a ScreenCapture using DirectX 9.
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/d3d9/nf-d3d9-idirect3ddevice9-getfrontbufferdata
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public sealed class DX9ScreenCapture : AbstractScreenCapture<ColorBGRA>
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly object _captureLock = new();
|
||||
|
||||
private readonly IDirect3D9 _direct3D9;
|
||||
private IDirect3DDevice9? _device;
|
||||
private IDirect3DSurface9? _surface;
|
||||
private byte[]? _buffer;
|
||||
private int _stride;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DX9ScreenCapture"/> class.
|
||||
/// </summary>
|
||||
/// <param name="direct3D9">The D3D9 instance used.</param>
|
||||
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
|
||||
internal DX9ScreenCapture(IDirect3D9 direct3D9, Display display)
|
||||
: base(display)
|
||||
{
|
||||
this._direct3D9 = direct3D9;
|
||||
|
||||
Restart();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool PerformScreenCapture()
|
||||
{
|
||||
bool result = false;
|
||||
lock (_captureLock)
|
||||
{
|
||||
if ((_device == null) || (_surface == null) || (_buffer == null))
|
||||
{
|
||||
Restart();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_device.GetFrontBufferData(0, _surface);
|
||||
|
||||
LockedRectangle dr = _surface.LockRect(LockFlags.NoSystemLock | LockFlags.ReadOnly);
|
||||
|
||||
nint ptr = dr.DataPointer;
|
||||
for (int y = 0; y < Display.Height; y++)
|
||||
{
|
||||
Marshal.Copy(ptr, _buffer, y * _stride, _stride);
|
||||
ptr += dr.Pitch;
|
||||
}
|
||||
|
||||
_surface.UnlockRect();
|
||||
|
||||
result = true;
|
||||
}
|
||||
catch (SharpGenException dxException)
|
||||
{
|
||||
if (dxException.ResultCode == Result.AccessDenied)
|
||||
{
|
||||
try
|
||||
{
|
||||
Restart();
|
||||
}
|
||||
catch { Thread.Sleep(100); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
if (_buffer == null) return;
|
||||
|
||||
using IDisposable @lock = captureZone.Lock();
|
||||
{
|
||||
if (captureZone.DownscaleLevel == 0)
|
||||
CopyZone(captureZone, buffer);
|
||||
else
|
||||
DownscaleZone(captureZone, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void CopyZone(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
RefImage<ColorBGRA>.Wrap(_buffer, Display.Width, Display.Height, _stride)[captureZone.X, captureZone.Y, captureZone.Width, captureZone.Height]
|
||||
.CopyTo(MemoryMarshal.Cast<byte, ColorBGRA>(buffer));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void DownscaleZone(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
RefImage<ColorBGRA> source = RefImage<ColorBGRA>.Wrap(_buffer, Display.Width, Display.Height, _stride)[captureZone.X, captureZone.Y, captureZone.UnscaledWidth, captureZone.UnscaledHeight];
|
||||
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
|
||||
|
||||
int blockSize = 1 << captureZone.DownscaleLevel;
|
||||
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
target[(y * width) + x] = source[x * blockSize, y * blockSize, blockSize, blockSize].Average();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Restart()
|
||||
{
|
||||
base.Restart();
|
||||
|
||||
lock (_captureLock)
|
||||
{
|
||||
DisposeDX();
|
||||
|
||||
try
|
||||
{
|
||||
PresentParameters presentParameters = new()
|
||||
{
|
||||
BackBufferWidth = Display.Width,
|
||||
BackBufferHeight = Display.Height,
|
||||
Windowed = true,
|
||||
SwapEffect = SwapEffect.Discard
|
||||
};
|
||||
_device = _direct3D9.CreateDevice(Display.Index, DeviceType.Hardware, IntPtr.Zero, CreateFlags.SoftwareVertexProcessing, presentParameters);
|
||||
_surface = _device.CreateOffscreenPlainSurface(Display.Width, Display.Height, Format.A8R8G8B8, Pool.Scratch);
|
||||
_stride = Display.Width * ColorBGRA.ColorFormat.BytesPerPixel;
|
||||
_buffer = new byte[Display.Height * _stride];
|
||||
}
|
||||
catch
|
||||
{
|
||||
DisposeDX();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
lock (_captureLock)
|
||||
DisposeDX();
|
||||
}
|
||||
|
||||
private void DisposeDX()
|
||||
{
|
||||
try
|
||||
{
|
||||
try { _surface?.Dispose(); } catch { /**/}
|
||||
try { _device?.Dispose(); } catch { /**/}
|
||||
_buffer = null;
|
||||
_device = null;
|
||||
_surface = null;
|
||||
}
|
||||
catch { /**/ }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
97
ScreenCapture.NET.DX9/DX9ScreenCaptureService.cs
Normal file
97
ScreenCapture.NET.DX9/DX9ScreenCaptureService.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Vortice.Direct3D9;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a <see cref="IScreenCaptureService"/> using the <see cref="DX9ScreenCapture"/>.
|
||||
/// </summary>
|
||||
public class DX9ScreenCaptureService : IScreenCaptureService
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly IDirect3D9 _direct3D9;
|
||||
private readonly Dictionary<Display, DX9ScreenCapture> _screenCaptures = new();
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DX9ScreenCaptureService"/> class.
|
||||
/// </summary>
|
||||
public DX9ScreenCaptureService()
|
||||
{
|
||||
_direct3D9 = D3D9.Direct3DCreate9();
|
||||
}
|
||||
|
||||
~DX9ScreenCaptureService() => Dispose();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<GraphicsCard> GetGraphicsCards()
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
Dictionary<int, GraphicsCard> graphicsCards = new();
|
||||
for (int i = 0; i < _direct3D9.AdapterCount; i++)
|
||||
{
|
||||
AdapterIdentifier adapterIdentifier = _direct3D9.GetAdapterIdentifier(i);
|
||||
if (!graphicsCards.ContainsKey(adapterIdentifier.DeviceId))
|
||||
graphicsCards.Add(adapterIdentifier.DeviceId, new GraphicsCard(i, adapterIdentifier.Description, adapterIdentifier.VendorId, adapterIdentifier.DeviceId));
|
||||
}
|
||||
|
||||
return graphicsCards.Values;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Display> GetDisplays(GraphicsCard graphicsCard)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
for (int i = 0; i < _direct3D9.AdapterCount; i++)
|
||||
{
|
||||
AdapterIdentifier adapterIdentifier = _direct3D9.GetAdapterIdentifier(i);
|
||||
if (adapterIdentifier.DeviceId == graphicsCard.DeviceId)
|
||||
{
|
||||
DisplayMode displayMode = _direct3D9.GetAdapterDisplayMode(i);
|
||||
yield return new Display(i, adapterIdentifier.DeviceName, displayMode.Width, displayMode.Height, Rotation.None, graphicsCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
|
||||
public DX9ScreenCapture GetScreenCapture(Display display)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
if (!_screenCaptures.TryGetValue(display, out DX9ScreenCapture? screenCapture))
|
||||
_screenCaptures.Add(display, screenCapture = new DX9ScreenCapture(_direct3D9, display));
|
||||
return screenCapture;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
foreach (DX9ScreenCapture screenCapture in _screenCaptures.Values)
|
||||
screenCapture.Dispose();
|
||||
_screenCaptures.Clear();
|
||||
|
||||
try { _direct3D9.Dispose(); } catch { /**/ }
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
BIN
ScreenCapture.NET.DX9/Resources/icon.png
Normal file
BIN
ScreenCapture.NET.DX9/Resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 B |
72
ScreenCapture.NET.DX9/ScreenCapture.NET.DX9.csproj
Normal file
72
ScreenCapture.NET.DX9/ScreenCapture.NET.DX9.csproj
Normal file
@ -0,0 +1,72 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<Authors>Darth Affe</Authors>
|
||||
<Company>Wyrez</Company>
|
||||
<Language>en-US</Language>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Title>ScreenCapture.NET.DX9</Title>
|
||||
<AssemblyName>ScreenCapture.NET.DX9</AssemblyName>
|
||||
<AssemblyTitle>ScreenCapture.NET.DX9</AssemblyTitle>
|
||||
<PackageId>ScreenCapture.NET.DX9</PackageId>
|
||||
<RootNamespace>ScreenCapture.NET</RootNamespace>
|
||||
<Description>DirectX 9 based Screen-Capturing</Description>
|
||||
<Summary>DirectX 9 based Screen-Capturing. Use DX11 if possible!</Summary>
|
||||
<Copyright>Copyright © Darth Affe 2024</Copyright>
|
||||
<PackageCopyright>Copyright © Darth Affe 2024</PackageCopyright>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/DarthAffe/ScreenCapture.NET</PackageProjectUrl>
|
||||
<PackageLicenseExpression>LGPL-2.1-only</PackageLicenseExpression>
|
||||
<RepositoryType>Github</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/DarthAffe/ScreenCapture.NET</RepositoryUrl>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
|
||||
<PackageReleaseNotes>
|
||||
</PackageReleaseNotes>
|
||||
|
||||
<Version>3.0.0</Version>
|
||||
<AssemblyVersion>3.0.0</AssemblyVersion>
|
||||
<FileVersion>3.0.0</FileVersion>
|
||||
|
||||
<OutputPath>..\bin\</OutputPath>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IncludeSource>True</IncludeSource>
|
||||
<IncludeSymbols>True</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<DefineConstants>$(DefineConstants);TRACE;DEBUG</DefineConstants>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>$(NoWarn);CS1591;CS1572;CS1573</NoWarn>
|
||||
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Resources\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Vortice.Direct3D9" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
BIN
ScreenCapture.NET.X11/Resources/icon.png
Normal file
BIN
ScreenCapture.NET.X11/Resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 B |
68
ScreenCapture.NET.X11/ScreenCapture.NET.X11.csproj
Normal file
68
ScreenCapture.NET.X11/ScreenCapture.NET.X11.csproj
Normal file
@ -0,0 +1,68 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<Authors>Darth Affe</Authors>
|
||||
<Company>Wyrez</Company>
|
||||
<Language>en-US</Language>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Title>ScreenCapture.NET.X11</Title>
|
||||
<AssemblyName>ScreenCapture.NET.X11</AssemblyName>
|
||||
<AssemblyTitle>ScreenCapture.NET.X11</AssemblyTitle>
|
||||
<PackageId>ScreenCapture.NET.X11</PackageId>
|
||||
<RootNamespace>ScreenCapture.NET</RootNamespace>
|
||||
<Description>libX11 based Screen-Capturing</Description>
|
||||
<Summary>libX11 based Screen-Capturing</Summary>
|
||||
<Copyright>Copyright © Darth Affe 2024</Copyright>
|
||||
<PackageCopyright>Copyright © Darth Affe 2024</PackageCopyright>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/DarthAffe/ScreenCapture.NET</PackageProjectUrl>
|
||||
<PackageLicenseExpression>LGPL-2.1-only</PackageLicenseExpression>
|
||||
<RepositoryType>Github</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/DarthAffe/ScreenCapture.NET</RepositoryUrl>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
|
||||
<PackageReleaseNotes>
|
||||
</PackageReleaseNotes>
|
||||
|
||||
<Version>3.0.0</Version>
|
||||
<AssemblyVersion>3.0.0</AssemblyVersion>
|
||||
<FileVersion>3.0.0</FileVersion>
|
||||
|
||||
<OutputPath>..\bin\</OutputPath>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IncludeSource>True</IncludeSource>
|
||||
<IncludeSymbols>True</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<DefineConstants>$(DefineConstants);TRACE;DEBUG</DefineConstants>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>$(NoWarn);CS1591;CS1572;CS1573</NoWarn>
|
||||
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Resources\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
67
ScreenCapture.NET.X11/X11.cs
Normal file
67
ScreenCapture.NET.X11/X11.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
internal static partial class X11
|
||||
{
|
||||
internal const nint DISPLAY_NAME = 0;
|
||||
|
||||
internal const long ALL_PLANES = -1;
|
||||
internal const int ZPIXMAP = 2;
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial nint XOpenDisplay(nint displayName);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial int XScreenCount(nint display);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial nint XScreenOfDisplay(nint display, int screeenNumber);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial int XWidthOfScreen(nint screen);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial int XHeightOfScreen(nint screen);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial nint XRootWindowOfScreen(nint screen);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial nint XGetImage(nint display, nint drawable, int x, int y, uint width, uint height, long planeMask, int format);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial nint XGetSubImage(nint display, nint drawable, int x, int y, uint width, uint height, long planeMask, int format, nint image, int destX, int dextY);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial void XDestroyImage(nint image);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial nint XDisplayString(nint display);
|
||||
|
||||
[LibraryImport("libX11.so.6")]
|
||||
internal static partial void XCloseDisplay(nint display);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal unsafe struct XImage
|
||||
{
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public int width;
|
||||
public int height;
|
||||
public int xoffset;
|
||||
public int format;
|
||||
public byte* data;
|
||||
public int byte_order;
|
||||
public int bitmap_unit;
|
||||
public int bitmap_bit_order;
|
||||
public int bitmap_pad;
|
||||
public int depth;
|
||||
public int bytes_per_line;
|
||||
public int bits_per_pixel;
|
||||
public uint red_mask;
|
||||
public uint green_mask;
|
||||
public uint blue_mask;
|
||||
public nint obdata;
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
}
|
||||
}
|
||||
147
ScreenCapture.NET.X11/X11ScreenCapture.cs
Normal file
147
ScreenCapture.NET.X11/X11ScreenCapture.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using HPPH;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a ScreenCapture using libX11.
|
||||
/// https://x.org/releases/current/doc/libX11/libX11/libX11.html#XGetImage
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public sealed class X11ScreenCapture : AbstractScreenCapture<ColorBGRA>
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly object _captureLock = new();
|
||||
|
||||
private nint _display;
|
||||
private nint _drawable;
|
||||
private nint _imageHandle;
|
||||
private X11.XImage _image;
|
||||
private int _size;
|
||||
private unsafe ReadOnlySpan<byte> Data => new(_image.data, _size);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="X11ScreenCapture"/> class.
|
||||
/// </summary>
|
||||
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
|
||||
internal X11ScreenCapture(Display display)
|
||||
: base(display)
|
||||
{
|
||||
Restart();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool PerformScreenCapture()
|
||||
{
|
||||
lock (_captureLock)
|
||||
{
|
||||
if ((_display == 0) || (_imageHandle == 0))
|
||||
{
|
||||
Restart();
|
||||
return false;
|
||||
}
|
||||
|
||||
X11.XGetSubImage(_display, _drawable, 0, 0, (uint)Display.Width, (uint)Display.Height, X11.ALL_PLANES, X11.ZPIXMAP, _imageHandle, 0, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
using IDisposable @lock = captureZone.Lock();
|
||||
{
|
||||
if (captureZone.DownscaleLevel == 0)
|
||||
CopyZone(captureZone, buffer);
|
||||
else
|
||||
DownscaleZone(captureZone, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void CopyZone(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
RefImage<ColorBGRA>.Wrap(Data, Display.Width, Display.Height, _image.bytes_per_line)[captureZone.X, captureZone.Y, captureZone.Width, captureZone.Height]
|
||||
.CopyTo(MemoryMarshal.Cast<byte, ColorBGRA>(buffer));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void DownscaleZone(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
|
||||
{
|
||||
RefImage<ColorBGRA> source = RefImage<ColorBGRA>.Wrap(Data, Display.Width, Display.Height, _image.bytes_per_line)[captureZone.X, captureZone.Y, captureZone.UnscaledWidth, captureZone.UnscaledHeight];
|
||||
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
|
||||
|
||||
int blockSize = 1 << captureZone.DownscaleLevel;
|
||||
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
target[(y * width) + x] = source[x * blockSize, y * blockSize, blockSize, blockSize].Average();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Restart()
|
||||
{
|
||||
base.Restart();
|
||||
|
||||
lock (_captureLock)
|
||||
{
|
||||
DisposeDisplay();
|
||||
try
|
||||
{
|
||||
_display = X11.XOpenDisplay(X11.DISPLAY_NAME);
|
||||
|
||||
nint screen = X11.XScreenOfDisplay(_display, Display.Index);
|
||||
_drawable = X11.XRootWindowOfScreen(screen);
|
||||
_imageHandle = X11.XGetImage(_display, _drawable, 0, 0, (uint)Display.Width, (uint)Display.Height, X11.ALL_PLANES, X11.ZPIXMAP);
|
||||
_image = Marshal.PtrToStructure<X11.XImage>(_imageHandle);
|
||||
_size = _image.bytes_per_line * _image.height;
|
||||
|
||||
if (_image.bits_per_pixel != (ColorBGRA.ColorFormat.BytesPerPixel * 8)) throw new NotSupportedException("The X-Server is configured to a not supported pixel-format. Needs to be 32 bit per pixel BGR.");
|
||||
}
|
||||
catch
|
||||
{
|
||||
DisposeDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
lock (_captureLock)
|
||||
{
|
||||
try { DisposeDisplay(); }
|
||||
catch { /**/ }
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeDisplay()
|
||||
{
|
||||
if (_imageHandle != 0)
|
||||
try { X11.XDestroyImage(_imageHandle); } catch { /**/ }
|
||||
|
||||
_image = default;
|
||||
|
||||
if (_display != 0)
|
||||
try { X11.XCloseDisplay(_display); } catch { /**/ }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
104
ScreenCapture.NET.X11/X11ScreenCaptureService.cs
Normal file
104
ScreenCapture.NET.X11/X11ScreenCaptureService.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a <see cref="IScreenCaptureService"/> using the <see cref="X11ScreenCapture"/>.
|
||||
/// </summary>
|
||||
public class X11ScreenCaptureService : IScreenCaptureService
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly Dictionary<Display, X11ScreenCapture> _screenCaptures = new();
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="X11ScreenCaptureService"/> class.
|
||||
/// </summary>
|
||||
public X11ScreenCaptureService()
|
||||
{ }
|
||||
|
||||
~X11ScreenCaptureService() => Dispose();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<GraphicsCard> GetGraphicsCards()
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
nint display = X11.XOpenDisplay(X11.DISPLAY_NAME);
|
||||
try
|
||||
{
|
||||
string name = Marshal.PtrToStringAnsi(X11.XDisplayString(display)) ?? string.Empty;
|
||||
yield return new GraphicsCard(0, name, 0, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
X11.XCloseDisplay(display);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Display> GetDisplays(GraphicsCard graphicsCard)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
nint display = X11.XOpenDisplay(X11.DISPLAY_NAME);
|
||||
try
|
||||
{
|
||||
int screenCount = X11.XScreenCount(display);
|
||||
for (int screenNumber = 0; screenNumber < screenCount; screenNumber++)
|
||||
{
|
||||
nint screen = X11.XScreenOfDisplay(display, screenNumber);
|
||||
int screenWidth = X11.XWidthOfScreen(screen);
|
||||
int screenHeight = X11.XHeightOfScreen(screen);
|
||||
|
||||
// DarthAffe 10.09.2023: Emulate DX-Displaynames for no real reason ¯\(°_o)/¯
|
||||
yield return new Display(screenNumber, @$"\\.\DISPLAY{screenNumber + 1}", screenWidth, screenHeight, Rotation.None, graphicsCard);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
X11.XCloseDisplay(display);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
|
||||
|
||||
/// <inheritdoc cref="IScreenCaptureService.GetScreenCapture"/>
|
||||
public X11ScreenCapture GetScreenCapture(Display display)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
if (!_screenCaptures.TryGetValue(display, out X11ScreenCapture? screenCapture))
|
||||
_screenCaptures.Add(display, screenCapture = new X11ScreenCapture(display));
|
||||
return screenCapture;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
foreach (X11ScreenCapture screenCapture in _screenCaptures.Values)
|
||||
screenCapture.Dispose();
|
||||
_screenCaptures.Clear();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,9 +1,19 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31025.194
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.6.33829.357
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenCapture.NET", "ScreenCapture.NET\ScreenCapture.NET.csproj", "{90596344-E012-4534-A933-3BD1B55469DC}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET", "ScreenCapture.NET\ScreenCapture.NET.csproj", "{90596344-E012-4534-A933-3BD1B55469DC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX11", "ScreenCapture.NET.DX11\ScreenCapture.NET.DX11.csproj", "{58A09AD8-D66F-492E-8BC7-62BDB85E57EC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CF7A1475-3A44-4870-A80F-5988DA25418B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.Tests", "Tests\ScreenCapture.NET.Tests\ScreenCapture.NET.Tests.csproj", "{AA1829BB-EFA7-4BB8-8041-76374659A42B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX9", "ScreenCapture.NET.DX9\ScreenCapture.NET.DX9.csproj", "{27EB5B17-2F83-43BA-A21F-06D93948B8BF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.X11", "ScreenCapture.NET.X11\ScreenCapture.NET.X11.csproj", "{F81562C8-2035-4FB9-9547-C51F9D343BDF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -15,10 +25,29 @@ Global
|
||||
{90596344-E012-4534-A933-3BD1B55469DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{90596344-E012-4534-A933-3BD1B55469DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{90596344-E012-4534-A933-3BD1B55469DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F81562C8-2035-4FB9-9547-C51F9D343BDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F81562C8-2035-4FB9-9547-C51F9D343BDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{AA1829BB-EFA7-4BB8-8041-76374659A42B} = {CF7A1475-3A44-4870-A80F-5988DA25418B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B5111031-6E65-4331-9E6E-A07165289860}
|
||||
EndGlobalSection
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BGR/@EntryIndexedValue">BGR</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BGRA/@EntryIndexedValue">BGRA</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DPI/@EntryIndexedValue">DPI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DX/@EntryIndexedValue">DX</s:String></wpf:ResourceDictionary>
|
||||
@ -1,530 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using SharpGen.Runtime;
|
||||
using Vortice.Direct3D;
|
||||
using Vortice.Direct3D11;
|
||||
using Vortice.DXGI;
|
||||
using Vortice.Mathematics;
|
||||
using MapFlags = Vortice.Direct3D11.MapFlags;
|
||||
using ResultCode = Vortice.DXGI.ResultCode;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a ScreenCapture using DirectX 11 desktop duplicaton.
|
||||
/// https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public sealed class DX11ScreenCapture : IScreenCapture
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private static readonly FeatureLevel[] FEATURE_LEVELS =
|
||||
{
|
||||
FeatureLevel.Level_11_1,
|
||||
FeatureLevel.Level_11_0,
|
||||
FeatureLevel.Level_10_1,
|
||||
FeatureLevel.Level_10_0
|
||||
};
|
||||
|
||||
private const int BPP = 4;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly object _captureLock = new();
|
||||
|
||||
private readonly bool _useNewDuplicationAdapter;
|
||||
private int _indexCounter = 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Display Display { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout in ms used for screen-capturing. (default 1000ms)
|
||||
/// This is used in <see cref="CaptureScreen"/> https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-acquirenextframe
|
||||
/// </summary>
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public int Timeout { get; set; } = 1000;
|
||||
|
||||
private readonly IDXGIFactory1 _factory;
|
||||
|
||||
private IDXGIOutput? _output;
|
||||
private IDXGIOutputDuplication? _duplicatedOutput;
|
||||
private ID3D11Device? _device;
|
||||
private ID3D11DeviceContext? _context;
|
||||
private ID3D11Texture2D? _captureTexture;
|
||||
|
||||
private readonly Dictionary<CaptureZone, (ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? _scalingTextureView)> _captureZones = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<ScreenCaptureUpdatedEventArgs>? Updated;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DX11ScreenCapture"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that setting useNewDuplicationAdapter to true requires to call <c>DPIAwareness.Initalize();</c> and prevents the capture from running in a WPF-thread.
|
||||
/// </remarks>
|
||||
/// <param name="factory">The <see cref="IDXGIFactory1"/> used to create underlying objects.</param>
|
||||
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
|
||||
/// <param name="useNewDuplicationAdapter">Indicates if the DuplicateOutput1 interface should be used instead of the older DuplicateOutput. Currently there's no real use in setting this to true.</param>
|
||||
public DX11ScreenCapture(IDXGIFactory1 factory, Display display, bool useNewDuplicationAdapter = false)
|
||||
{
|
||||
this._factory = factory;
|
||||
this.Display = display;
|
||||
this._useNewDuplicationAdapter = useNewDuplicationAdapter;
|
||||
|
||||
Restart();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CaptureScreen()
|
||||
{
|
||||
bool result = false;
|
||||
lock (_captureLock)
|
||||
{
|
||||
if ((_context == null) || (_duplicatedOutput == null) || (_captureTexture == null))
|
||||
{
|
||||
Restart();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IDXGIResource? screenResource = null;
|
||||
try
|
||||
{
|
||||
_duplicatedOutput.AcquireNextFrame(Timeout, out OutduplFrameInfo duplicateFrameInformation, out screenResource).CheckError();
|
||||
if ((screenResource == null) || (duplicateFrameInformation.LastPresentTime == 0)) return false;
|
||||
|
||||
using ID3D11Texture2D screenTexture = screenResource.QueryInterface<ID3D11Texture2D>();
|
||||
_context.CopySubresourceRegion(_captureTexture, 0, 0, 0, 0, screenTexture, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
screenResource?.Dispose();
|
||||
_duplicatedOutput?.ReleaseFrame();
|
||||
}
|
||||
catch { /**/ }
|
||||
}
|
||||
|
||||
result = true;
|
||||
}
|
||||
catch (SharpGenException dxException)
|
||||
{
|
||||
if ((dxException.ResultCode == ResultCode.AccessLost)
|
||||
|| (dxException.ResultCode == ResultCode.AccessDenied)
|
||||
|| (dxException.ResultCode == ResultCode.InvalidCall))
|
||||
{
|
||||
try
|
||||
{
|
||||
Restart();
|
||||
}
|
||||
catch { Thread.Sleep(100); }
|
||||
}
|
||||
}
|
||||
catch { /**/ }
|
||||
|
||||
try
|
||||
{
|
||||
UpdateZones();
|
||||
}
|
||||
catch { /**/ }
|
||||
|
||||
try
|
||||
{
|
||||
Updated?.Invoke(this, new ScreenCaptureUpdatedEventArgs(result));
|
||||
}
|
||||
catch { /**/ }
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateZones()
|
||||
{
|
||||
if (_context == null) return;
|
||||
|
||||
lock (_captureZones)
|
||||
{
|
||||
foreach ((CaptureZone captureZone, (ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? scalingTextureView)) in _captureZones.Where(z => z.Key.AutoUpdate || z.Key.IsUpdateRequested))
|
||||
{
|
||||
if (scalingTexture != null)
|
||||
{
|
||||
_context.CopySubresourceRegion(scalingTexture, 0, 0, 0, 0, _captureTexture, 0,
|
||||
new Box(captureZone.X, captureZone.Y, 0,
|
||||
captureZone.X + captureZone.UnscaledWidth,
|
||||
captureZone.Y + captureZone.UnscaledHeight, 1));
|
||||
_context.GenerateMips(scalingTextureView);
|
||||
_context.CopySubresourceRegion(stagingTexture, 0, 0, 0, 0, scalingTexture, captureZone.DownscaleLevel);
|
||||
}
|
||||
else
|
||||
_context.CopySubresourceRegion(stagingTexture, 0, 0, 0, 0, _captureTexture, 0,
|
||||
new Box(captureZone.X, captureZone.Y, 0,
|
||||
captureZone.X + captureZone.UnscaledWidth,
|
||||
captureZone.Y + captureZone.UnscaledHeight, 1));
|
||||
|
||||
MappedSubresource mapSource = _context.Map(stagingTexture, 0, MapMode.Read, MapFlags.None);
|
||||
lock (captureZone.Buffer)
|
||||
{
|
||||
Span<byte> source = mapSource.AsSpan(mapSource.RowPitch * captureZone.Height);
|
||||
switch (Display.Rotation)
|
||||
{
|
||||
case Rotation.Rotation90:
|
||||
CopyRotate90(source, mapSource.RowPitch, captureZone);
|
||||
break;
|
||||
|
||||
case Rotation.Rotation180:
|
||||
CopyRotate180(source, mapSource.RowPitch, captureZone);
|
||||
break;
|
||||
|
||||
case Rotation.Rotation270:
|
||||
CopyRotate270(source, mapSource.RowPitch, captureZone);
|
||||
break;
|
||||
|
||||
default:
|
||||
CopyRotate0(source, mapSource.RowPitch, captureZone);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_context.Unmap(stagingTexture, 0);
|
||||
captureZone.SetUpdated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate0(in Span<byte> source, int sourceStride, in CaptureZone captureZone)
|
||||
{
|
||||
int height = captureZone.Height;
|
||||
int stride = captureZone.Stride;
|
||||
Span<byte> target = captureZone.Buffer.AsSpan();
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
int sourceOffset = y * sourceStride;
|
||||
int targetOffset = y * stride;
|
||||
|
||||
source.Slice(sourceOffset, stride).CopyTo(target.Slice(targetOffset, stride));
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate90(in Span<byte> source, int sourceStride, in CaptureZone captureZone)
|
||||
{
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
Span<byte> target = captureZone.Buffer.AsSpan();
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int sourceOffset = ((y * sourceStride) + (x * BPP));
|
||||
int targetOffset = ((x * height) + ((height - 1) - y)) * BPP;
|
||||
|
||||
target[targetOffset] = source[sourceOffset];
|
||||
target[targetOffset + 1] = source[sourceOffset + 1];
|
||||
target[targetOffset + 2] = source[sourceOffset + 2];
|
||||
target[targetOffset + 3] = source[sourceOffset + 3];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate180(in Span<byte> source, int sourceStride, in CaptureZone captureZone)
|
||||
{
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
int stride = captureZone.Stride;
|
||||
Span<byte> target = captureZone.Buffer.AsSpan();
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int sourceOffset = ((y * sourceStride) + (x * BPP));
|
||||
int targetOffset = target.Length - ((y * stride) + (x * BPP)) - 1;
|
||||
|
||||
target[targetOffset - 3] = source[sourceOffset];
|
||||
target[targetOffset - 2] = source[sourceOffset + 1];
|
||||
target[targetOffset - 1] = source[sourceOffset + 2];
|
||||
target[targetOffset] = source[sourceOffset + 3];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyRotate270(in Span<byte> source, int sourceStride, in CaptureZone captureZone)
|
||||
{
|
||||
int width = captureZone.Width;
|
||||
int height = captureZone.Height;
|
||||
Span<byte> target = captureZone.Buffer.AsSpan();
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int sourceOffset = ((y * sourceStride) + (x * BPP));
|
||||
int targetOffset = ((((width - 1) - x) * height) + y) * BPP;
|
||||
|
||||
target[targetOffset] = source[sourceOffset];
|
||||
target[targetOffset + 1] = source[sourceOffset + 1];
|
||||
target[targetOffset + 2] = source[sourceOffset + 2];
|
||||
target[targetOffset + 3] = source[sourceOffset + 3];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0)
|
||||
{
|
||||
ValidateCaptureZoneAndThrow(x, y, width, height);
|
||||
|
||||
if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
|
||||
(x, y, width, height) = (y, x, height, width);
|
||||
|
||||
int unscaledWidth = width;
|
||||
int unscaledHeight = height;
|
||||
(width, height) = CalculateScaledSize(unscaledWidth, unscaledHeight, downscaleLevel);
|
||||
|
||||
byte[] buffer = new byte[width * height * BPP];
|
||||
|
||||
CaptureZone captureZone = new(_indexCounter++, x, y, width, height, BPP, downscaleLevel, unscaledWidth, unscaledHeight, buffer);
|
||||
lock (_captureZones)
|
||||
InitializeCaptureZone(captureZone);
|
||||
|
||||
return captureZone;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UnregisterCaptureZone(CaptureZone captureZone)
|
||||
{
|
||||
lock (_captureZones)
|
||||
{
|
||||
if (_captureZones.TryGetValue(captureZone, out (ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? _scalingTextureView) data))
|
||||
{
|
||||
_captureZones.Remove(captureZone);
|
||||
data.stagingTexture.Dispose();
|
||||
data.scalingTexture?.Dispose();
|
||||
data._scalingTextureView?.Dispose();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null)
|
||||
{
|
||||
lock (_captureZones)
|
||||
if (!_captureZones.ContainsKey(captureZone))
|
||||
throw new ArgumentException("The capture zone is not registered to this ScreenCapture", nameof(captureZone));
|
||||
|
||||
int newX = x ?? captureZone.X;
|
||||
int newY = y ?? captureZone.Y;
|
||||
int newUnscaledWidth = width ?? captureZone.UnscaledWidth;
|
||||
int newUnscaledHeight = height ?? captureZone.UnscaledHeight;
|
||||
int newDownscaleLevel = downscaleLevel ?? captureZone.DownscaleLevel;
|
||||
|
||||
ValidateCaptureZoneAndThrow(newX, newY, newUnscaledWidth, newUnscaledHeight);
|
||||
|
||||
if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
|
||||
(newX, newY, newUnscaledWidth, newUnscaledHeight) = (newY, newX, newUnscaledHeight, newUnscaledWidth);
|
||||
|
||||
captureZone.X = newX;
|
||||
captureZone.Y = newY;
|
||||
|
||||
//TODO DarthAffe 01.05.2022: For now just reinitialize the zone in that case, but this could be optimized to only recreate the textures needed.
|
||||
if ((width != null) || (height != null) || (downscaleLevel != null))
|
||||
{
|
||||
(int newWidth, int newHeight) = CalculateScaledSize(newUnscaledWidth, newUnscaledHeight, newDownscaleLevel);
|
||||
lock (_captureZones)
|
||||
{
|
||||
UnregisterCaptureZone(captureZone);
|
||||
|
||||
captureZone.UnscaledWidth = newUnscaledWidth;
|
||||
captureZone.UnscaledHeight = newUnscaledHeight;
|
||||
captureZone.Width = newWidth;
|
||||
captureZone.Height = newHeight;
|
||||
captureZone.DownscaleLevel = newDownscaleLevel;
|
||||
captureZone.Buffer = new byte[newWidth * newHeight * BPP];
|
||||
|
||||
InitializeCaptureZone(captureZone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private (int width, int height) CalculateScaledSize(int width, int height, int downscaleLevel)
|
||||
{
|
||||
if (downscaleLevel > 0)
|
||||
for (int i = 0; i < downscaleLevel; i++)
|
||||
{
|
||||
width /= 2;
|
||||
height /= 2;
|
||||
}
|
||||
|
||||
if (width < 1) width = 1;
|
||||
if (height < 1) height = 1;
|
||||
|
||||
return (width, height);
|
||||
}
|
||||
|
||||
private void ValidateCaptureZoneAndThrow(int x, int y, int width, int height)
|
||||
{
|
||||
if (_device == null) throw new ApplicationException("ScreenCapture isn't initialized.");
|
||||
|
||||
if (x < 0) throw new ArgumentException("x < 0");
|
||||
if (y < 0) throw new ArgumentException("y < 0");
|
||||
if (width <= 0) throw new ArgumentException("with <= 0");
|
||||
if (height <= 0) throw new ArgumentException("height <= 0");
|
||||
if ((x + width) > Display.Width) throw new ArgumentException("x + width > Display width");
|
||||
if ((y + height) > Display.Height) throw new ArgumentException("y + height > Display height");
|
||||
}
|
||||
|
||||
private void InitializeCaptureZone(in CaptureZone captureZone)
|
||||
{
|
||||
Texture2DDescription stagingTextureDesc = new()
|
||||
{
|
||||
CPUAccessFlags = CpuAccessFlags.Read,
|
||||
BindFlags = BindFlags.None,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Width = captureZone.Width,
|
||||
Height = captureZone.Height,
|
||||
MiscFlags = ResourceOptionFlags.None,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Staging
|
||||
};
|
||||
ID3D11Texture2D stagingTexture = _device!.CreateTexture2D(stagingTextureDesc);
|
||||
|
||||
ID3D11Texture2D? scalingTexture = null;
|
||||
ID3D11ShaderResourceView? scalingTextureView = null;
|
||||
if (captureZone.DownscaleLevel > 0)
|
||||
{
|
||||
Texture2DDescription scalingTextureDesc = new()
|
||||
{
|
||||
CPUAccessFlags = CpuAccessFlags.None,
|
||||
BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Width = captureZone.UnscaledWidth,
|
||||
Height = captureZone.UnscaledHeight,
|
||||
MiscFlags = ResourceOptionFlags.GenerateMips,
|
||||
MipLevels = captureZone.DownscaleLevel + 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Default
|
||||
};
|
||||
scalingTexture = _device!.CreateTexture2D(scalingTextureDesc);
|
||||
scalingTextureView = _device.CreateShaderResourceView(scalingTexture);
|
||||
}
|
||||
|
||||
_captureZones[captureZone] = (stagingTexture, scalingTexture, scalingTextureView);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Restart()
|
||||
{
|
||||
lock (_captureLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<CaptureZone> captureZones = _captureZones.Keys.ToList();
|
||||
Dispose();
|
||||
|
||||
using IDXGIAdapter1 adapter = _factory.GetAdapter1(Display.GraphicsCard.Index) ?? throw new ApplicationException("Couldn't create DirectX-Adapter.");
|
||||
|
||||
D3D11.D3D11CreateDevice(adapter, DriverType.Unknown, DeviceCreationFlags.None, FEATURE_LEVELS, out _device).CheckError();
|
||||
_context = _device!.ImmediateContext;
|
||||
|
||||
_output = adapter.GetOutput(Display.Index) ?? throw new ApplicationException("Couldn't get DirectX-Output.");
|
||||
using IDXGIOutput5 output = _output.QueryInterface<IDXGIOutput5>();
|
||||
|
||||
int width = Display.Width;
|
||||
int height = Display.Height;
|
||||
if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
|
||||
(width, height) = (height, width);
|
||||
|
||||
Texture2DDescription captureTextureDesc = new()
|
||||
{
|
||||
CPUAccessFlags = CpuAccessFlags.None,
|
||||
BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Width = width,
|
||||
Height = height,
|
||||
MiscFlags = ResourceOptionFlags.None,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Default
|
||||
};
|
||||
_captureTexture = _device.CreateTexture2D(captureTextureDesc);
|
||||
|
||||
lock (_captureZones)
|
||||
{
|
||||
foreach (CaptureZone captureZone in captureZones)
|
||||
InitializeCaptureZone(captureZone);
|
||||
}
|
||||
|
||||
if (_useNewDuplicationAdapter)
|
||||
_duplicatedOutput = output.DuplicateOutput1(_device, new[] { Format.B8G8R8A8_UNorm }); // DarthAffe 27.02.2021: This prepares for the use of 10bit color depth
|
||||
else
|
||||
_duplicatedOutput = output.DuplicateOutput(_device);
|
||||
}
|
||||
catch { Dispose(false); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
private void Dispose(bool removeCaptureZones)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_captureLock)
|
||||
{
|
||||
try { _duplicatedOutput?.Dispose(); } catch { /**/ }
|
||||
_duplicatedOutput = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (removeCaptureZones)
|
||||
{
|
||||
List<CaptureZone> captureZones = _captureZones.Keys.ToList();
|
||||
foreach (CaptureZone captureZone in captureZones)
|
||||
UnregisterCaptureZone(captureZone);
|
||||
}
|
||||
}
|
||||
catch { /**/ }
|
||||
|
||||
try { _output?.Dispose(); } catch { /**/ }
|
||||
try { _context?.Dispose(); } catch { /**/ }
|
||||
try { _device?.Dispose(); } catch { /**/ }
|
||||
try { _captureTexture?.Dispose(); } catch { /**/ }
|
||||
_context = null;
|
||||
_captureTexture = null;
|
||||
}
|
||||
}
|
||||
catch { /**/ }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
356
ScreenCapture.NET/Extensions/BlackBarDetection.cs
Normal file
356
ScreenCapture.NET/Extensions/BlackBarDetection.cs
Normal file
@ -0,0 +1,356 @@
|
||||
using HPPH;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Helper-class for black-bar removal.
|
||||
/// </summary>
|
||||
public static class BlackBarDetection
|
||||
{
|
||||
#region IImage
|
||||
|
||||
/// <summary>
|
||||
/// Create an image with black bars removed
|
||||
/// </summary>
|
||||
/// <param name="image">The image the bars are removed from.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <param name="removeTop">A bool indicating if black bars should be removed at the top of the image.</param>
|
||||
/// <param name="removeBottom">A bool indicating if black bars should be removed at the bottom of the image.</param>
|
||||
/// <param name="removeLeft">A bool indicating if black bars should be removed on the left side of the image.</param>
|
||||
/// <param name="removeRight">A bool indicating if black bars should be removed on the right side of the image.</param>
|
||||
/// <returns>The image with black bars removed.</returns>
|
||||
public static IImage RemoveBlackBars(this IImage image, int threshold = 0, bool removeTop = true, bool removeBottom = true, bool removeLeft = true, bool removeRight = true)
|
||||
{
|
||||
int top = removeTop ? CalculateTop(image, threshold) : 0;
|
||||
int bottom = removeBottom ? CalculateBottom(image, threshold) : image.Height;
|
||||
int left = removeLeft ? CalculateLeft(image, threshold) : 0;
|
||||
int right = removeRight ? CalculateRight(image, threshold) : image.Width;
|
||||
|
||||
return image[left, top, right - left, bottom - top];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the first row starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The row number of the first row with at least one non-black pixel.</returns>
|
||||
public static int CalculateTop(IImage image, int threshold)
|
||||
{
|
||||
IImageRows rows = image.Rows;
|
||||
for (int y = 0; y < rows.Count; y++)
|
||||
{
|
||||
IImageRow row = rows[y];
|
||||
foreach (IColor color in row)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the last row starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The row number of the last row with at least one non-black pixel.</returns>
|
||||
public static int CalculateBottom(IImage image, int threshold)
|
||||
{
|
||||
IImageRows rows = image.Rows;
|
||||
for (int y = rows.Count - 1; y >= 0; y--)
|
||||
{
|
||||
IImageRow row = rows[y];
|
||||
foreach (IColor color in row)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the first column starting from the left with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The column number of the first column with at least one non-black pixel.</returns>
|
||||
public static int CalculateLeft(IImage image, int threshold)
|
||||
{
|
||||
IImageColumns columns = image.Columns;
|
||||
for (int x = 0; x < columns.Count; x++)
|
||||
{
|
||||
IImageColumn column = columns[x];
|
||||
foreach (IColor color in column)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the last column starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The column number of the last column with at least one non-black pixel.</returns>
|
||||
public static int CalculateRight(IImage image, int threshold)
|
||||
{
|
||||
IImageColumns columns = image.Columns;
|
||||
for (int x = columns.Count - 1; x >= 0; x--)
|
||||
{
|
||||
IImageColumn column = columns[x];
|
||||
foreach (IColor color in column)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return columns.Count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Image
|
||||
|
||||
/// <summary>
|
||||
/// Create an image with black bars removed
|
||||
/// </summary>
|
||||
/// <param name="image">The image the bars are removed from.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <param name="removeTop">A bool indicating if black bars should be removed at the top of the image.</param>
|
||||
/// <param name="removeBottom">A bool indicating if black bars should be removed at the bottom of the image.</param>
|
||||
/// <param name="removeLeft">A bool indicating if black bars should be removed on the left side of the image.</param>
|
||||
/// <param name="removeRight">A bool indicating if black bars should be removed on the right side of the image.</param>
|
||||
/// <returns>The image with black bars removed.</returns>
|
||||
public static RefImage<T> RemoveBlackBars<T>(this IImage<T> image, int threshold = 0, bool removeTop = true, bool removeBottom = true, bool removeLeft = true, bool removeRight = true)
|
||||
where T : struct, IColor
|
||||
{
|
||||
int top = removeTop ? CalculateTop(image, threshold) : 0;
|
||||
int bottom = removeBottom ? CalculateBottom(image, threshold) : image.Height;
|
||||
int left = removeLeft ? CalculateLeft(image, threshold) : 0;
|
||||
int right = removeRight ? CalculateRight(image, threshold) : image.Width;
|
||||
|
||||
return image[left, top, right - left, bottom - top];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the first row starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The row number of the first row with at least one non-black pixel.</returns>
|
||||
public static int CalculateTop<T>(IImage<T> image, int threshold)
|
||||
where T : struct, IColor
|
||||
{
|
||||
ImageRows<T> rows = image.Rows;
|
||||
for (int y = 0; y < rows.Count; y++)
|
||||
{
|
||||
ImageRow<T> row = rows[y];
|
||||
foreach (T color in row)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the last row starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The row number of the last row with at least one non-black pixel.</returns>
|
||||
public static int CalculateBottom<T>(IImage<T> image, int threshold)
|
||||
where T : struct, IColor
|
||||
{
|
||||
ImageRows<T> rows = image.Rows;
|
||||
for (int y = rows.Count - 1; y >= 0; y--)
|
||||
{
|
||||
ImageRow<T> row = rows[y];
|
||||
foreach (T color in row)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the first column starting from the left with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The column number of the first column with at least one non-black pixel.</returns>
|
||||
public static int CalculateLeft<T>(IImage<T> image, int threshold)
|
||||
where T : struct, IColor
|
||||
{
|
||||
ImageColumns<T> columns = image.Columns;
|
||||
for (int x = 0; x < columns.Count; x++)
|
||||
{
|
||||
ImageColumn<T> column = columns[x];
|
||||
foreach (T color in column)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the last column starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The column number of the last column with at least one non-black pixel.</returns>
|
||||
public static int CalculateRight<T>(IImage<T> image, int threshold)
|
||||
where T : struct, IColor
|
||||
{
|
||||
ImageColumns<T> columns = image.Columns;
|
||||
for (int x = columns.Count - 1; x >= 0; x--)
|
||||
{
|
||||
ImageColumn<T> column = columns[x];
|
||||
foreach (T color in column)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return columns.Count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region RefImage
|
||||
|
||||
/// <summary>
|
||||
/// Create an image with black bars removed
|
||||
/// </summary>
|
||||
/// <param name="image">The image the bars are removed from.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <param name="removeTop">A bool indicating if black bars should be removed at the top of the image.</param>
|
||||
/// <param name="removeBottom">A bool indicating if black bars should be removed at the bottom of the image.</param>
|
||||
/// <param name="removeLeft">A bool indicating if black bars should be removed on the left side of the image.</param>
|
||||
/// <param name="removeRight">A bool indicating if black bars should be removed on the right side of the image.</param>
|
||||
/// <returns>The image with black bars removed.</returns>
|
||||
public static RefImage<TColor> RemoveBlackBars<TColor>(this RefImage<TColor> image, int threshold = 0, bool removeTop = true, bool removeBottom = true, bool removeLeft = true, bool removeRight = true)
|
||||
where TColor : struct, IColor
|
||||
{
|
||||
int top = removeTop ? CalculateTop(image, threshold) : 0;
|
||||
int bottom = removeBottom ? CalculateBottom(image, threshold) : image.Height - 1;
|
||||
int left = removeLeft ? CalculateLeft(image, threshold) : 0;
|
||||
int right = removeRight ? CalculateRight(image, threshold) : image.Width - 1;
|
||||
|
||||
return image[left, top, (right - left) + 1, (bottom - top) + 1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the first row starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The row number of the first row with at least one non-black pixel.</returns>
|
||||
public static int CalculateTop<TColor>(this RefImage<TColor> image, int threshold)
|
||||
where TColor : struct, IColor
|
||||
{
|
||||
ImageRows<TColor> rows = image.Rows;
|
||||
for (int y = 0; y < rows.Count; y++)
|
||||
{
|
||||
ImageRow<TColor> row = rows[y];
|
||||
foreach (TColor color in row)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the last row starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The row number of the last row with at least one non-black pixel.</returns>
|
||||
public static int CalculateBottom<TColor>(this RefImage<TColor> image, int threshold)
|
||||
where TColor : struct, IColor
|
||||
{
|
||||
ImageRows<TColor> rows = image.Rows;
|
||||
for (int y = rows.Count - 1; y >= 0; y--)
|
||||
{
|
||||
ImageRow<TColor> row = rows[y];
|
||||
foreach (TColor color in row)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the first column starting from the left with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The column number of the first column with at least one non-black pixel.</returns>
|
||||
public static int CalculateLeft<TColor>(this RefImage<TColor> image, int threshold)
|
||||
where TColor : struct, IColor
|
||||
{
|
||||
ImageColumns<TColor> columns = image.Columns;
|
||||
for (int x = 0; x < columns.Count; x++)
|
||||
{
|
||||
ImageColumn<TColor> column = columns[x];
|
||||
foreach (TColor color in column)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the last column starting from the top with at least one non black pixel.
|
||||
/// </summary>
|
||||
/// <param name="image">The image to check.</param>
|
||||
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
|
||||
/// <returns>The column number of the last column with at least one non-black pixel.</returns>
|
||||
public static int CalculateRight<TColor>(this RefImage<TColor> image, int threshold)
|
||||
where TColor : struct, IColor
|
||||
{
|
||||
ImageColumns<TColor> columns = image.Columns;
|
||||
for (int x = columns.Count - 1; x >= 0; x--)
|
||||
{
|
||||
ImageColumn<TColor> column = columns[x];
|
||||
foreach (TColor color in column)
|
||||
{
|
||||
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
return columns.Count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
248
ScreenCapture.NET/Generic/AbstractScreenCapture.cs
Normal file
248
ScreenCapture.NET/Generic/AbstractScreenCapture.cs
Normal file
@ -0,0 +1,248 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HPPH;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class AbstractScreenCapture<TColor> : IScreenCapture
|
||||
where TColor : struct, IColor
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of <see cref="CaptureZone{TColol}"/> registered on this ScreenCapture.
|
||||
/// </summary>
|
||||
protected HashSet<CaptureZone<TColor>> CaptureZones { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Display Display { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<ScreenCaptureUpdatedEventArgs>? Updated;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AbstractScreenCapture{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
|
||||
protected AbstractScreenCapture(Display display)
|
||||
{
|
||||
this.Display = display;
|
||||
}
|
||||
|
||||
~AbstractScreenCapture() => Dispose(false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool CaptureScreen()
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
bool result;
|
||||
|
||||
try
|
||||
{
|
||||
result = PerformScreenCapture();
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = false;
|
||||
}
|
||||
|
||||
lock (CaptureZones)
|
||||
foreach (CaptureZone<TColor> captureZone in CaptureZones.Where(x => x.AutoUpdate || x.IsUpdateRequested))
|
||||
{
|
||||
try
|
||||
{
|
||||
PerformCaptureZoneUpdate(captureZone, captureZone.InternalBuffer);
|
||||
captureZone.SetUpdated();
|
||||
}
|
||||
catch { /* */ }
|
||||
}
|
||||
|
||||
OnUpdated(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the actual screen capture.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the screen was captured sucessfully; otherwise, <c>false</c>.</returns>
|
||||
protected abstract bool PerformScreenCapture();
|
||||
|
||||
/// <summary>
|
||||
/// Performs an update of the given capture zone.
|
||||
/// </summary>
|
||||
/// <param name="captureZone">The capture zone to update.</param>
|
||||
/// <param name="buffer">The buffer containing the current pixel-data of the capture zone.</param>
|
||||
protected abstract void PerformCaptureZoneUpdate(CaptureZone<TColor> captureZone, Span<byte> buffer);
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="Updated"/>-event.
|
||||
/// </summary>
|
||||
/// <param name="result">A bool indicating whether the update was successful or not.</param>
|
||||
protected virtual void OnUpdated(bool result)
|
||||
{
|
||||
try
|
||||
{
|
||||
Updated?.Invoke(this, new ScreenCaptureUpdatedEventArgs(result));
|
||||
}
|
||||
catch { /**/ }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
ICaptureZone IScreenCapture.RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel) => RegisterCaptureZone(x, y, width, height, downscaleLevel);
|
||||
|
||||
/// <inheritdoc cref="IScreenCapture.RegisterCaptureZone" />
|
||||
public virtual CaptureZone<TColor> RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
lock (CaptureZones)
|
||||
{
|
||||
ValidateCaptureZoneAndThrow(x, y, width, height, downscaleLevel);
|
||||
|
||||
int unscaledWidth = width;
|
||||
int unscaledHeight = height;
|
||||
(width, height, downscaleLevel) = CalculateScaledSize(unscaledWidth, unscaledHeight, downscaleLevel);
|
||||
|
||||
CaptureZone<TColor> captureZone = new(Display, x, y, width, height, downscaleLevel, unscaledWidth, unscaledHeight);
|
||||
CaptureZones.Add(captureZone);
|
||||
|
||||
return captureZone;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the given values of a capture zone.
|
||||
/// </summary>
|
||||
/// <param name="x">The X-location of the zone.</param>
|
||||
/// <param name="y">The Y-location of the zone.</param>
|
||||
/// <param name="width">The width of the zone.</param>
|
||||
/// <param name="height">The height of the zone.</param>
|
||||
/// <param name="downscaleLevel">The downscale-level of the zone.</param>
|
||||
/// <exception cref="ArgumentException">Throws if some of the provided data is not valid.</exception>
|
||||
protected virtual void ValidateCaptureZoneAndThrow(int x, int y, int width, int height, int downscaleLevel)
|
||||
{
|
||||
if (x < 0) throw new ArgumentException("x < 0");
|
||||
if (y < 0) throw new ArgumentException("y < 0");
|
||||
if (width <= 0) throw new ArgumentException("with <= 0");
|
||||
if (height <= 0) throw new ArgumentException("height <= 0");
|
||||
if ((x + width) > Display.Width) throw new ArgumentException("x + width > Display width");
|
||||
if ((y + height) > Display.Height) throw new ArgumentException("y + height > Display height");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the actual size when downscaling is used.
|
||||
/// </summary>
|
||||
/// <param name="width">The original width.</param>
|
||||
/// <param name="height">The original height.</param>
|
||||
/// <param name="downscaleLevel">The level of downscaling to be used.</param>
|
||||
/// <returns>A tuple containing the scaled width, the scaled height and the downscale-level used. (This can be smaller then the one provided if the image is not big enough to scale down that often.)</returns>
|
||||
protected virtual (int width, int height, int downscaleLevel) CalculateScaledSize(int width, int height, int downscaleLevel)
|
||||
{
|
||||
if (downscaleLevel > 0)
|
||||
for (int i = 0; i < downscaleLevel; i++)
|
||||
{
|
||||
if ((width <= 1) && (height <= 1))
|
||||
{
|
||||
downscaleLevel = i;
|
||||
break;
|
||||
}
|
||||
|
||||
width /= 2;
|
||||
height /= 2;
|
||||
}
|
||||
|
||||
if (width < 1) width = 1;
|
||||
if (height < 1) height = 1;
|
||||
|
||||
return (width, height, downscaleLevel);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
bool IScreenCapture.UnregisterCaptureZone(ICaptureZone captureZone) => UnregisterCaptureZone(captureZone as CaptureZone<TColor> ?? throw new ArgumentException("Invalid capture-zone."));
|
||||
|
||||
/// <inheritdoc cref="IScreenCapture.UnregisterCaptureZone" />
|
||||
public virtual bool UnregisterCaptureZone(CaptureZone<TColor> captureZone)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
return CaptureZones.Remove(captureZone);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IScreenCapture.UpdateCaptureZone(ICaptureZone captureZone, int? x, int? y, int? width, int? height, int? downscaleLevel)
|
||||
=> UpdateCaptureZone(captureZone as CaptureZone<TColor> ?? throw new ArgumentException("Invalid capture-zone."), x, y, width, height, downscaleLevel);
|
||||
|
||||
/// <inheritdoc cref="IScreenCapture.UpdateCaptureZone" />
|
||||
public virtual void UpdateCaptureZone(CaptureZone<TColor> captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null)
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
|
||||
lock (CaptureZones)
|
||||
{
|
||||
if (!CaptureZones.Contains(captureZone))
|
||||
throw new ArgumentException("The capture zone is not registered to this ScreenCapture", nameof(captureZone));
|
||||
|
||||
int newX = x ?? captureZone.X;
|
||||
int newY = y ?? captureZone.Y;
|
||||
int newUnscaledWidth = width ?? captureZone.UnscaledWidth;
|
||||
int newUnscaledHeight = height ?? captureZone.UnscaledHeight;
|
||||
int newDownscaleLevel = downscaleLevel ?? captureZone.DownscaleLevel;
|
||||
|
||||
ValidateCaptureZoneAndThrow(newX, newY, newUnscaledWidth, newUnscaledHeight, newDownscaleLevel);
|
||||
|
||||
captureZone.X = newX;
|
||||
captureZone.Y = newY;
|
||||
|
||||
if ((width != null) || (height != null) || (downscaleLevel != null))
|
||||
{
|
||||
(int newWidth, int newHeight, newDownscaleLevel) = CalculateScaledSize(newUnscaledWidth, newUnscaledHeight, newDownscaleLevel);
|
||||
captureZone.Resize(newWidth, newHeight, newDownscaleLevel, newUnscaledWidth, newUnscaledHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Restart()
|
||||
{
|
||||
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
catch { /* don't throw in dispose! */ }
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IDisposable.Dispose" />
|
||||
protected virtual void Dispose(bool disposing) { }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -18,7 +18,7 @@ public interface IScreenCapture : IDisposable
|
||||
event EventHandler<ScreenCaptureUpdatedEventArgs>? Updated;
|
||||
|
||||
/// <summary>
|
||||
/// Attemts to capture the current frame showed on the <see cref="Display"/>.
|
||||
/// Attempts to capture the current frame showed on the <see cref="Display"/>.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the current frame was captures successfully; otherwise, <c>false</c>.</returns>
|
||||
bool CaptureScreen();
|
||||
@ -32,14 +32,14 @@ public interface IScreenCapture : IDisposable
|
||||
/// <param name="height">The height of the region to capture (must be >= 0 and this + y must be <= screen-height).</param>
|
||||
/// <param name="downscaleLevel">The level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.</param>
|
||||
/// <returns>The new <see cref="CaptureScreen"/>.</returns>
|
||||
CaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0);
|
||||
ICaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the given <see cref="CaptureScreen"/> from the <see cref="IScreenCapture"/>.
|
||||
/// </summary>
|
||||
/// <param name="captureZone">The previously registered <see cref="CaptureScreen"/>.</param>
|
||||
/// <returns><c>true</c> if the <see cref="CaptureScreen"/> was successfully removed; otherwise, <c>false</c>.</returns>
|
||||
bool UnregisterCaptureZone(CaptureZone captureZone);
|
||||
bool UnregisterCaptureZone(ICaptureZone captureZone);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the the given <see cref="CaptureScreen"/>.
|
||||
@ -53,10 +53,10 @@ public interface IScreenCapture : IDisposable
|
||||
/// <param name="width">The width of the region to capture (must be >= 0 and this + x must be <= screen-width).</param>
|
||||
/// <param name="height">The new height of the region to capture (must be >= 0 and this + y must be <= screen-height).</param>
|
||||
/// <param name="downscaleLevel">The new level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.</param>
|
||||
void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null);
|
||||
void UpdateCaptureZone(ICaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null);
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the <see cref="IScreenCapture"/>.
|
||||
/// </summary>
|
||||
void Restart();
|
||||
}
|
||||
}
|
||||
@ -1,141 +0,0 @@
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
||||
using System;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for the detection and removal of black bars around the screen image.
|
||||
/// </summary>
|
||||
public sealed class BlackBarDetection
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly CaptureZone _captureZone;
|
||||
|
||||
private int? _top;
|
||||
/// <summary>
|
||||
/// Gets the size of the detected black bar at the top of the image.
|
||||
/// </summary>
|
||||
public int Top => _top ??= CalculateTop();
|
||||
|
||||
private int? _bottom;
|
||||
/// <summary>
|
||||
/// Gets the size of the detected black bar at the bottom of the image.
|
||||
/// </summary>
|
||||
public int Bottom => _bottom ??= CalculateBottom();
|
||||
|
||||
private int? _left;
|
||||
/// <summary>
|
||||
/// Gets the size of the detected black bar at the left of the image.
|
||||
/// </summary>
|
||||
public int Left => _left ??= CalculateLeft();
|
||||
|
||||
private int? _right;
|
||||
/// <summary>
|
||||
/// Gets the size of the detected black bar at the right of the image.
|
||||
/// </summary>
|
||||
public int Right => _right ??= CalculateRight();
|
||||
|
||||
private int _theshold = 0;
|
||||
/// <summary>
|
||||
/// Gets or sets the threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.) (default 0)
|
||||
/// </summary>
|
||||
public int Threshold
|
||||
{
|
||||
get => _theshold;
|
||||
set
|
||||
{
|
||||
_theshold = value;
|
||||
InvalidateCache();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
internal BlackBarDetection(CaptureZone captureZone)
|
||||
{
|
||||
this._captureZone = captureZone;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates the cached values and recalculates <see cref="Top"/>, <see cref="Bottom"/>, <see cref="Left"/> and <see cref="Right"/>.
|
||||
/// </summary>
|
||||
public void InvalidateCache()
|
||||
{
|
||||
_top = null;
|
||||
_bottom = null;
|
||||
_left = null;
|
||||
_right = null;
|
||||
}
|
||||
|
||||
private int CalculateTop()
|
||||
{
|
||||
int threshold = Threshold;
|
||||
int stride = _captureZone.Stride;
|
||||
for (int row = 0; row < _captureZone.Height; row++)
|
||||
{
|
||||
Span<byte> data = new(_captureZone.Buffer, row * stride, stride);
|
||||
for (int i = 0; i < data.Length; i += 4)
|
||||
if ((data[i] > threshold) || (data[i + 1] > threshold) || (data[i + 2] > threshold))
|
||||
return row;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int CalculateBottom()
|
||||
{
|
||||
int threshold = Threshold;
|
||||
int stride = _captureZone.Stride;
|
||||
for (int row = _captureZone.Height - 1; row >= 0; row--)
|
||||
{
|
||||
Span<byte> data = new(_captureZone.Buffer, row * stride, stride);
|
||||
for (int i = 0; i < data.Length; i += 4)
|
||||
if ((data[i] > threshold) || (data[i + 1] > threshold) || (data[i + 2] > threshold))
|
||||
return (_captureZone.Height - 1) - row;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int CalculateLeft()
|
||||
{
|
||||
int threshold = Threshold;
|
||||
int stride = _captureZone.Stride;
|
||||
byte[] buffer = _captureZone.Buffer;
|
||||
for (int column = 0; column < _captureZone.Width; column++)
|
||||
for (int row = 0; row < _captureZone.Height; row++)
|
||||
{
|
||||
int offset = (stride * row) + (column * 4);
|
||||
if ((buffer[offset] > threshold) || (buffer[offset + 1] > threshold) || (buffer[offset + 2] > threshold))
|
||||
return column;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int CalculateRight()
|
||||
{
|
||||
int threshold = Threshold;
|
||||
int stride = _captureZone.Stride;
|
||||
byte[] buffer = _captureZone.Buffer;
|
||||
for (int column = _captureZone.Width - 1; column >= 0; column--)
|
||||
for (int row = 0; row < _captureZone.Height; row++)
|
||||
{
|
||||
int offset = (stride * row) + (column * 4);
|
||||
if ((buffer[offset] > threshold) || (buffer[offset + 1] > threshold) || (buffer[offset + 2] > threshold))
|
||||
return (_captureZone.Width - 1) - column;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,93 +1,107 @@
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using HPPH;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a duplicated region on the screen.
|
||||
/// </summary>
|
||||
public sealed class CaptureZone
|
||||
public sealed class CaptureZone<TColor> : ICaptureZone
|
||||
where TColor : struct, IColor
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique id of this <see cref="CaptureZone"/>.
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the x-location of the region on the screen.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public Display Display { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IColorFormat ColorFormat
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => TColor.ColorFormat;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int X { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the y-location of the region on the screen.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public int Y { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the captured region.
|
||||
/// </summary>
|
||||
public int Width { get; internal set; }
|
||||
/// <inheritdoc />
|
||||
public int Width { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Height { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Stride
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => Width * ColorFormat.BytesPerPixel;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int DownscaleLevel { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int UnscaledWidth { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int UnscaledHeight { get; private set; }
|
||||
|
||||
internal byte[] InternalBuffer { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlySpan<byte> RawBuffer
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => InternalBuffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the captured region.
|
||||
/// Gets the pixel-data of this zone.
|
||||
/// </summary>
|
||||
public int Height { get; internal set; }
|
||||
public ReadOnlySpan<TColor> Pixels
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => MemoryMarshal.Cast<byte, TColor>(RawBuffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.
|
||||
/// Gets a <see cref="IImage{TColor}"/>.
|
||||
/// </summary>
|
||||
public int DownscaleLevel { get; internal set; }
|
||||
public IImage<TColor> Image => Image<TColor>.Wrap(InternalBuffer, Width, Height, Stride);
|
||||
|
||||
/// <inheritdoc />
|
||||
IImage ICaptureZone.Image => Image;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original width of the region (this equals <see cref="Width"/> if <see cref="DownscaleLevel"/> is 0).
|
||||
/// Gets a <see cref="RefImage{TColor}"/>.
|
||||
/// </summary>
|
||||
public int UnscaledWidth { get; internal set; }
|
||||
public RefImage<TColor> RefImage
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => RefImage<TColor>.Wrap(RawBuffer, Width, Height, Stride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original height of the region (this equals <see cref="Height"/> if <see cref="DownscaleLevel"/> is 0).
|
||||
/// </summary>
|
||||
public int UnscaledHeight { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of bytes per pixel in the image (most likely 3 [RGB] or 4 [ARGB]).
|
||||
/// </summary>
|
||||
public int BytesPerPixel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size in bytes of a row in the region (<see cref="Width"/> * <see cref="BytesPerPixel"/>).
|
||||
/// </summary>
|
||||
public int Stride => Width * BytesPerPixel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the buffer containing the image data. Format depends on the specific capture but is most likely BGRA32.
|
||||
/// </summary>
|
||||
public byte[] Buffer { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the config for black-bar detection.
|
||||
/// </summary>
|
||||
public BlackBarDetection BlackBars { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the <see cref="CaptureZone"/> should be automatically updated on every captured frame.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool AutoUpdate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets if an update for the <see cref="CaptureZone"/> is requested on the next captured frame.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsUpdateRequested { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the <see cref="CaptureZone"/> is updated.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? Updated;
|
||||
|
||||
#endregion
|
||||
@ -95,9 +109,9 @@ public sealed class CaptureZone
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CaptureZone"/> class.
|
||||
/// Initializes a new instance of the <see cref="CaptureZone{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique id of this <see cref="CaptureZone"/>.</param>
|
||||
/// <param name="id">The unique id of this <see cref="CaptureZone{T}"/>.</param>
|
||||
/// <param name="x">The x-location of the region on the screen.</param>
|
||||
/// <param name="y">The y-location of the region on the screen.</param>
|
||||
/// <param name="width">The width of the region on the screen.</param>
|
||||
@ -107,56 +121,97 @@ public sealed class CaptureZone
|
||||
/// <param name="unscaledWidth">The original width of the region.</param>
|
||||
/// <param name="unscaledHeight">The original height of the region</param>
|
||||
/// <param name="buffer">The buffer containing the image data.</param>
|
||||
internal CaptureZone(int id, int x, int y, int width, int height, int bytesPerPixel, int downscaleLevel, int unscaledWidth, int unscaledHeight, byte[] buffer)
|
||||
internal CaptureZone(Display display, int x, int y, int width, int height, int downscaleLevel, int unscaledWidth, int unscaledHeight)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Display = display;
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.BytesPerPixel = bytesPerPixel;
|
||||
this.DownscaleLevel = downscaleLevel;
|
||||
this.UnscaledWidth = unscaledWidth;
|
||||
this.UnscaledHeight = unscaledHeight;
|
||||
this.DownscaleLevel = downscaleLevel;
|
||||
this.Buffer = buffer;
|
||||
|
||||
BlackBars = new BlackBarDetection(this);
|
||||
InternalBuffer = new byte[Stride * Height];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Requests to update this <see cref="CaptureZone"/> when the next frame is captured.
|
||||
/// Only necessary if <see cref="AutoUpdate"/> is set to <c>false</c>.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public RefImage<T> GetRefImage<T>()
|
||||
where T : struct, IColor
|
||||
{
|
||||
if (typeof(T) != typeof(TColor)) throw new ArgumentException("The requested Color-Format does not match the data.", nameof(T));
|
||||
return RefImage<T>.Wrap(RawBuffer, Width, Height, Stride);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable Lock()
|
||||
{
|
||||
Monitor.Enter(_lock);
|
||||
return new UnlockDisposable(_lock);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RequestUpdate() => IsUpdateRequested = true;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the <see cref="CaptureZone"/> as updated.
|
||||
/// WARNING: This should not be called outside of an <see cref="IScreenCapture"/>!
|
||||
/// Marks the <see cref="CaptureZone{T}"/> as updated.
|
||||
/// </summary>
|
||||
public void SetUpdated()
|
||||
internal void SetUpdated()
|
||||
{
|
||||
IsUpdateRequested = false;
|
||||
BlackBars.InvalidateCache();
|
||||
|
||||
Updated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this <see cref="CaptureZone"/> equals the given one.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="CaptureZone"/> to compare.</param>
|
||||
/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
|
||||
public bool Equals(CaptureZone other) => Id == other.Id;
|
||||
internal void Resize(int width, int height, int downscaleLevel, int unscaledWidth, int unscaledHeight)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
DownscaleLevel = downscaleLevel;
|
||||
UnscaledWidth = unscaledWidth;
|
||||
UnscaledHeight = unscaledHeight;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is CaptureZone other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode() => Id;
|
||||
int newBufferSize = Stride * Height;
|
||||
if (newBufferSize != InternalBuffer.Length)
|
||||
InternalBuffer = new byte[newBufferSize];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private class UnlockDisposable : IDisposable
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private bool _disposed = false;
|
||||
private readonly object _lock;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
// ReSharper disable once ConvertToPrimaryConstructor
|
||||
public UnlockDisposable(object @lock) => this._lock = @lock;
|
||||
~UnlockDisposable() => Dispose();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
Monitor.Exit(_lock);
|
||||
_disposed = true;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
104
ScreenCapture.NET/Model/ICaptureZone.cs
Normal file
104
ScreenCapture.NET/Model/ICaptureZone.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using HPPH;
|
||||
|
||||
namespace ScreenCapture.NET;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a zone on the screen to be captured.
|
||||
/// </summary>
|
||||
public interface ICaptureZone
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the display this zone is on.
|
||||
/// </summary>
|
||||
Display Display { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color-format used in the buffer of this zone.
|
||||
/// </summary>
|
||||
IColorFormat ColorFormat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the x-location of the region on the screen.
|
||||
/// </summary>
|
||||
int X { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the y-location of the region on the screen.
|
||||
/// </summary>
|
||||
int Y { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the captured region.
|
||||
/// </summary>
|
||||
int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the captured region.
|
||||
/// </summary>
|
||||
int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stride of the buffer.
|
||||
/// </summary>
|
||||
int Stride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.
|
||||
/// </summary>
|
||||
int DownscaleLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original width of the region (this equals <see cref="Width"/> if <see cref="DownscaleLevel"/> is 0).
|
||||
/// </summary>
|
||||
int UnscaledWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original height of the region (this equals <see cref="Height"/> if <see cref="DownscaleLevel"/> is 0).
|
||||
/// </summary>
|
||||
int UnscaledHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw buffer of this zone.
|
||||
/// </summary>
|
||||
ReadOnlySpan<byte> RawBuffer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IImage"/> represented by the buffer of this zone.
|
||||
/// </summary>
|
||||
IImage Image { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the <see cref="ICaptureZone"/> should be automatically updated on every captured frame.
|
||||
/// </summary>
|
||||
bool AutoUpdate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets if an update for the <see cref="ICaptureZone"/> is requested on the next captured frame.
|
||||
/// </summary>
|
||||
bool IsUpdateRequested { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the <see cref="ICaptureZone"/> is updated.
|
||||
/// </summary>
|
||||
event EventHandler? Updated;
|
||||
|
||||
/// <summary>
|
||||
/// Locks the image for use. Unlock by disposing the returned disposable.
|
||||
/// </summary>
|
||||
/// <returns>The disposable used to unlock the image.</returns>
|
||||
IDisposable Lock();
|
||||
|
||||
/// <summary>
|
||||
/// Requests to update this <see cref="ICaptureZone"/> when the next frame is captured.
|
||||
/// Only necessary if <see cref="AutoUpdate"/> is set to <c>false</c>.
|
||||
/// </summary>
|
||||
void RequestUpdate();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="RefImage{TColor}"/>. Basically the same as <see cref="Image"/> but with better performance if the color-layout is known.
|
||||
/// </summary>
|
||||
/// <typeparam name="TColor">The color used by the buffer of this zone.</typeparam>
|
||||
/// <returns>The <see cref="RefImage{TColor}"/> representing this zone.</returns>
|
||||
RefImage<TColor> GetRefImage<TColor>() where TColor : struct, IColor;
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
@ -14,10 +14,10 @@
|
||||
<AssemblyTitle>ScreenCapture.NET</AssemblyTitle>
|
||||
<PackageId>ScreenCapture.NET</PackageId>
|
||||
<RootNamespace>ScreenCapture.NET</RootNamespace>
|
||||
<Description>Vortice based Screen-Capturing</Description>
|
||||
<Summary>Vortice based Screen-Capturing using Desktop Duplication</Summary>
|
||||
<Copyright>Copyright © Darth Affe 2023</Copyright>
|
||||
<PackageCopyright>Copyright © Darth Affe 2023</PackageCopyright>
|
||||
<Description>Core functionality for Screen-Capturing</Description>
|
||||
<Summary>Base package for ScreenCapture.NET projects</Summary>
|
||||
<Copyright>Copyright © Darth Affe 2024</Copyright>
|
||||
<PackageCopyright>Copyright © Darth Affe 2024</PackageCopyright>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/DarthAffe/ScreenCapture.NET</PackageProjectUrl>
|
||||
<PackageLicenseExpression>LGPL-2.1-only</PackageLicenseExpression>
|
||||
@ -26,12 +26,11 @@
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
|
||||
<PackageReleaseNotes>
|
||||
- Fixed striding issue on downscaled captures introduced with 1.3.0
|
||||
</PackageReleaseNotes>
|
||||
|
||||
<Version>1.3.1</Version>
|
||||
<AssemblyVersion>1.3.1</AssemblyVersion>
|
||||
<FileVersion>1.3.1</FileVersion>
|
||||
<Version>3.0.0</Version>
|
||||
<AssemblyVersion>3.0.0</AssemblyVersion>
|
||||
<FileVersion>3.0.0</FileVersion>
|
||||
|
||||
<OutputPath>..\bin\</OutputPath>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@ -62,7 +61,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Vortice.Direct3D11" Version="2.4.2" />
|
||||
<PackageReference Include="HPPH" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=communitytoolkit_002Ehighperformance/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=directx/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=generic/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helper/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model_005Ccolors/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
25
Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj
Normal file
25
Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
38
Tests/ScreenCapture.NET.Tests/TestScreenCapture.cs
Normal file
38
Tests/ScreenCapture.NET.Tests/TestScreenCapture.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using HPPH;
|
||||
|
||||
namespace ScreenCapture.NET.Tests;
|
||||
|
||||
internal class TestScreenCapture : AbstractScreenCapture<ColorARGB>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public TestScreenCapture(Rotation rotation = Rotation.None)
|
||||
: base(new Display(0, "Test", 1920, 1080, rotation, new GraphicsCard(0, "Test", 0, 0)))
|
||||
{ }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
protected override bool PerformScreenCapture() => true;
|
||||
|
||||
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorARGB> captureZone, Span<byte> buffer)
|
||||
{
|
||||
Span<ColorARGB> pixels = MemoryMarshal.Cast<byte, ColorARGB>(buffer);
|
||||
|
||||
for (int y = 0; y < captureZone.Height; y++)
|
||||
for (int x = 0; x < captureZone.Width; x++)
|
||||
pixels[(y * captureZone.Width) + x] = GetColorFromLocation(x, y);
|
||||
}
|
||||
|
||||
public static ColorARGB GetColorFromLocation(int x, int y)
|
||||
{
|
||||
byte[] xBytes = BitConverter.GetBytes((short)x);
|
||||
byte[] yBytes = BitConverter.GetBytes((short)y);
|
||||
return new ColorARGB(xBytes[0], xBytes[1], yBytes[0], yBytes[1]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user