Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,6 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# macOS
.DS_Store
**/.DS_Store
209 changes: 209 additions & 0 deletions ScreenCapture.NET.SCK/SCKScreenCapture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HPPH;
using ObjCRuntime;
using ScreenCapture.NET.SCK;
using ScreenCaptureKit;

namespace ScreenCapture.NET;
/// <summary>
/// Follow the architecture of this library created by author
/// This class create a session require for screen content (in mac os)
/// Capture screen for whatever interval (you have to implement a loop in calling class)
/// Then store at a buffer with the size calculated by color space and screen resolution
/// Everytime user register a new capture zone, a buffer corresponding for that zone is created
///
/// </summary>

public sealed class SCKScreenCapture : AbstractScreenCapture<ColorBGRA>
{
private SCDisplay _selectedDisplay;
private byte[]? _buffer;
private int _stride;
public double ScalingFactor => _scalingFactor;
private double _scalingFactor = 0.25d;
private SCContentFilter _filter;
private SCStreamConfiguration _streamConfig;
private SCStream _stream;
private ScreenCaptureDelegate _delegate;
public readonly object _captureLock = new();
public int BufferReceived => _delegate.BufferReceived;
private bool _isInitialized;
private int _blackFrameCounter;
private readonly object _lock = new();
#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="SCKScreenCapture"/> class.
/// </summary>
/// <param name="display">The <see cref="Display"/> to capture.</param>
/// <param name="scalingFactor">The scale factor value to start new video stream at.</param>
internal SCKScreenCapture(Display display, double scalingFactor)
: base(display)
{
_scalingFactor = scalingFactor;
Restart();
OnStreamComplete += StreamComplete;
}

private void StreamComplete(NSError error)
{
if (error != null)
{
_isInitialized = false;
_stream?.Dispose();
Thread.Sleep(1000);
}
else
{
_isInitialized = true;
}

}

public NativeHandle Handle { get; set; }

#endregion

/// <summary>
/// Perform a clean restart
/// </summary>
public override void Restart()
{
base.Restart();
lock (_captureLock)
{
try
{
SCShareableContent.GetShareableContent((SCShareableContent content, NSError error) =>
{
if (error != null)
{
//this is when user press denied, so we stop requesting
_isInitialized = true;
return;
}

//check if the display exist, feel silly enough because it actually happens when mac
// just wakeup from sleep
//the display is not available yet but the code is already running in background
if (content.Displays.Length <= Display.Index)
{
// Log.Error("Requested display not found" + Display.Index);
return;
}
_selectedDisplay = content.Displays[Display.Index];
_stride = (int)(Display.Width * _scalingFactor * ColorBGRA.ColorFormat.BytesPerPixel);
_buffer = new byte[(int)(Display.Height * _scalingFactor * _stride)];
var apps = content.Applications;
//config new sreen capture session
_filter = new SCContentFilter(_selectedDisplay, [], SCContentFilterOption.Exclude);
_streamConfig = new SCStreamConfiguration
{
Width = (nuint)(Display.Width * _scalingFactor),
Height = (nuint)(Display.Height * _scalingFactor),
MinimumFrameInterval = new CoreMedia.CMTime(1, 30), // 60 FPS
QueueDepth = 5,
PixelFormat = CoreVideo.CVPixelFormatType.CV32BGRA,
ScalesToFit = false,
SourceRect = new CGRect(0, 0, Display.Width, Display.Height),
ShowsCursor = false,
CaptureResolution = SCCaptureResolutionType.Best,
CapturesAudio = false,
StreamName = "SCKScreenCapture.NET"

};
//update registerd zones
_delegate = new ScreenCaptureDelegate(_buffer);
_delegate.StreamStopped += OnStreamStopped;
_stream = new SCStream(_filter, _streamConfig, _delegate);
var streamError = new NSError();
_stream.AddStreamOutput(_delegate, SCStreamOutputType.Screen, null, out streamError);
_stream.StartCapture(OnStreamComplete);

});

}
catch (Exception ex)
{

}
finally
{

}

}


}
private void OnStreamStopped()
{
_stream?.Dispose();
Thread.Sleep(1000);
_isInitialized = false;
}
private Action<NSError> OnStreamComplete;
private bool ReadyToRestart()
{
lock (_lock)
{
return !_isInitialized;
}
}
/// <inheritdoc />
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
{
if (ReadyToRestart())
{
_blackFrameCounter++;
if (_blackFrameCounter > 600)
{
//10 secs has passed, try to restart
Restart();
_blackFrameCounter = 0;
}
}

if (_buffer == null) return;
using IDisposable @lock = captureZone.Lock();
{
if (captureZone.DownscaleLevel == 0)
CopyZone(captureZone, buffer);
else
DownscaleZone(captureZone, buffer);
}
}

protected override bool PerformScreenCapture()
{
bool result = true;
//this will be handled in PerformCaptureZoneUpdate();
return result;
}


[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CopyZone(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
{
RefImage<ColorBGRA>.Wrap(_buffer, (int)(Display.Width * _scalingFactor), (int)(Display.Height * _scalingFactor), _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, (int)(Display.Width * _scalingFactor), (int)(Display.Height * _scalingFactor), _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();
}

}
81 changes: 81 additions & 0 deletions ScreenCapture.NET.SCK/SCKScreenCaptureService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;

namespace ScreenCapture.NET;

public class SCKScreenCaptureService : IScreenCaptureService
{
#region Properties & Fields


private bool _isDisposed;

#endregion

#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="SCKScreenCaptureService"/> class.
/// </summary>
public SCKScreenCaptureService()
{

}

private readonly Dictionary<Display, SCKScreenCapture> _screenCaptures = new();
~SCKScreenCaptureService() => Dispose();

#endregion

#region Methods

/// <inheritdoc />
public IEnumerable<GraphicsCard> GetGraphicsCards()
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);

Dictionary<int, GraphicsCard> graphicsCards = new();
// mac os doesn't need this
return graphicsCards.Values;
}

/// <inheritdoc />
public IEnumerable<Display> GetDisplays(GraphicsCard graphicsCard)

{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);

for (int i = 0; i < NSScreen.Screens.Length; i++)
{
yield return new Display(i, NSScreen.Screens[i].LocalizedName, (int)NSScreen.Screens[i].Frame.Width, (int)NSScreen.Screens[i].Frame.Height, Rotation.None, graphicsCard);
}
}

/// <inheritdoc />
IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
public SCKScreenCapture GetScreenCapture(Display display)
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);

if (!_screenCaptures.TryGetValue(display, out SCKScreenCapture? screenCapture))
_screenCaptures.Add(display, screenCapture = new SCKScreenCapture(display, 0.25f));
return screenCapture;
}

/// <inheritdoc />
public void Dispose()
{
if (_isDisposed) return;

foreach (SCKScreenCapture screenCapture in _screenCaptures.Values)
screenCapture.Dispose();
_screenCaptures.Clear();

//dispose sck

GC.SuppressFinalize(this);

_isDisposed = true;
}

#endregion
}
63 changes: 63 additions & 0 deletions ScreenCapture.NET.SCK/ScreenCapture.NET.SCK.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0-macos</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<Authors>Kaitoukid93</Authors>
<Company>Ambino</Company>
<Language>en-US</Language>
<NeutralLanguage>en-US</NeutralLanguage>
<Title>ScreenCapture.NET.SCK</Title>
<AssemblyName>ScreenCapture.NET.SCK</AssemblyName>
<AssemblyTitle>ScreenCapture.NET.SCK</AssemblyTitle>
<PackageId>ScreenCapture.NET.SCK</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>Screen Capture Kit based Screen-Capturing</Description>
<Summary>Screen Capture Kit based Screen-Capturing</Summary>
<Copyright>Copyright © Le Hoang Anh 2025</Copyright>
<PackageCopyright>Copyright © Le Hoang Anh 2025</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>
<ProjectReference Include="../ScreenCapture.NET/ScreenCapture.NET.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.0.1" />
</ItemGroup>

</Project>
Loading