From 4437977bcf9455553ceafd6d5e4e807f0a98490e Mon Sep 17 00:00:00 2001 From: zOe Date: Sat, 14 Mar 2026 13:36:22 +0700 Subject: [PATCH 1/2] Add ScreenCaptureKit capturing support for macOS --- .DS_Store | Bin 0 -> 8196 bytes ScreenCapture.NET.SCK/SCKScreenCapture.cs | 209 ++++++++++++++++++ .../SCKScreenCaptureService.cs | 81 +++++++ .../ScreenCapture.NET.SCK.csproj | 63 ++++++ .../ScreenCaptureDelegate.cs | 113 ++++++++++ ScreenCapture.NET.sln | 58 +++++ Tests/.DS_Store | Bin 0 -> 6148 bytes Tests/ScreenCapture.NET.Tests/.DS_Store | Bin 0 -> 6148 bytes docs/MacOs.md | 29 +++ 9 files changed, 553 insertions(+) create mode 100644 .DS_Store create mode 100644 ScreenCapture.NET.SCK/SCKScreenCapture.cs create mode 100644 ScreenCapture.NET.SCK/SCKScreenCaptureService.cs create mode 100644 ScreenCapture.NET.SCK/ScreenCapture.NET.SCK.csproj create mode 100644 ScreenCapture.NET.SCK/ScreenCaptureDelegate.cs create mode 100644 Tests/.DS_Store create mode 100644 Tests/ScreenCapture.NET.Tests/.DS_Store create mode 100644 docs/MacOs.md diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..302d408f28d3427749680fd22bd6bbca577c76ae GIT binary patch literal 8196 zcmeHMTWl0n7(U;$(3yeKQ)o-+2w7>utp&@a<*IIPPy_<9+Y1F;cXtMuFr6tov)dve zR*f%!MiY%s;wAbhi7^^ugy0+T!3R@OG%+S7zUYHNA2s@)IkUhPcrYY}ggMDM|NQ4a z|G9kU`{(SLWsISxXs%_fnK7nP7Lw1Q;tomJMZPU5QB5^TkUwL7Zo(hWIbQCzwsA#_ zK#V|)K#V|)K#aisfdHM^BB^&d_oX#1V+3LZ9!Lbl`yoMD$Ydm^qzqpjRD@RmlHv*w zCVHlOKG&dWO$0(X{uD2e#+>Lsr2jnc;OlEpKPubN3Z2VIR(W zPI1ijGrT^Zb@D|g8(7_rld_+&{E@(#48+r@?-hdHu;GYJzM?L}dC|A_TjXGutUhw{ z8)i%!+J(7|i*(C_c;CS8ebcq`>bR~iXc#WpzMXTd16e+bU>UJtos`cFdaQO$(bBwHStMbJM5X%SbV2pD5bFN#3fe(|lfC?GhuQA2Ml9 zdpuM0W~%&SEwr>63FDx!^cE~C&B$T7u~JX_v@DfpWy5xTjh;SEo2zkO)~wan8-q?Z zXE_=-tCL2L@st>Och(Q~j$5vn)fH*$ zC)=ohEL*VlP7uGMN$sTCaL1@#IQ5%$;Dts4WKY5{4v6l$O*33ZbJId@>^IUX|5~NZ zvE7nQoTg2e)K*)QtK|NKptdGEq^8j+wTP{uGiiVwqL>+HC)qjnCOgkQV4tu{>;eA}dMO?yVe1R|V4X)x_{DkZH1ApRgrAAQ|O_{GOP#Tpc zWu>x8S+8tTy5-cBa=eUI5>M$N{gyWX#Y^-BPdRa?cxlbQntdLyOC?b2FM`bPW>Ytw~kTme)kB zA6}{w_?=kNkvB)YzWSMeHN$9cR(#CQn8dOxQ^!@rP?xiD;;9(o)D;#-p%6v-+%Y-|MxUT>>MKyBXEBs zfa< +/// 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 +/// +/// + +public sealed class SCKScreenCapture : AbstractScreenCapture +{ + 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 + + /// + /// Initializes a new instance of the class. + /// + /// The to capture. + /// The scale factor value to start new video stream at. + 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 + + /// + /// Perform a clean restart + /// + 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 OnStreamComplete; + private bool ReadyToRestart() + { + lock (_lock) + { + return !_isInitialized; + } + } + /// + protected override void PerformCaptureZoneUpdate(CaptureZone captureZone, Span 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 captureZone, Span buffer) + { + RefImage.Wrap(_buffer, (int)(Display.Width * _scalingFactor), (int)(Display.Height * _scalingFactor), _stride)[captureZone.X, captureZone.Y, captureZone.Width, captureZone.Height] + .CopyTo(MemoryMarshal.Cast(buffer)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DownscaleZone(CaptureZone captureZone, Span buffer) + { + RefImage source = RefImage.Wrap(_buffer, (int)(Display.Width * _scalingFactor), (int)(Display.Height * _scalingFactor), _stride)[captureZone.X, captureZone.Y, captureZone.UnscaledWidth, captureZone.UnscaledHeight]; + Span target = MemoryMarshal.Cast(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(); + } + +} diff --git a/ScreenCapture.NET.SCK/SCKScreenCaptureService.cs b/ScreenCapture.NET.SCK/SCKScreenCaptureService.cs new file mode 100644 index 0000000..6d489c1 --- /dev/null +++ b/ScreenCapture.NET.SCK/SCKScreenCaptureService.cs @@ -0,0 +1,81 @@ +using System; + +namespace ScreenCapture.NET; + +public class SCKScreenCaptureService : IScreenCaptureService +{ + #region Properties & Fields + + + private bool _isDisposed; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public SCKScreenCaptureService() + { + + } + + private readonly Dictionary _screenCaptures = new(); + ~SCKScreenCaptureService() => Dispose(); + + #endregion + + #region Methods + + /// + public IEnumerable GetGraphicsCards() + { + if (_isDisposed) throw new ObjectDisposedException(GetType().FullName); + + Dictionary graphicsCards = new(); + // mac os doesn't need this + return graphicsCards.Values; + } + + /// + public IEnumerable 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); + } + } + + /// + 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; + } + + /// + public void Dispose() + { + if (_isDisposed) return; + + foreach (SCKScreenCapture screenCapture in _screenCaptures.Values) + screenCapture.Dispose(); + _screenCaptures.Clear(); + + //dispose sck + + GC.SuppressFinalize(this); + + _isDisposed = true; + } + + #endregion +} diff --git a/ScreenCapture.NET.SCK/ScreenCapture.NET.SCK.csproj b/ScreenCapture.NET.SCK/ScreenCapture.NET.SCK.csproj new file mode 100644 index 0000000..a115f79 --- /dev/null +++ b/ScreenCapture.NET.SCK/ScreenCapture.NET.SCK.csproj @@ -0,0 +1,63 @@ + + + + net8.0-macos + enable + enable + true + + Kaitoukid93 + Ambino + en-US + en-US + ScreenCapture.NET.SCK + ScreenCapture.NET.SCK + ScreenCapture.NET.SCK + ScreenCapture.NET.SCK + ScreenCapture.NET + Screen Capture Kit based Screen-Capturing + Screen Capture Kit based Screen-Capturing + Copyright © Le Hoang Anh 2025 + Copyright © Le Hoang Anh 2025 + + https://github.com/DarthAffe/ScreenCapture.NET + LGPL-2.1-only + Github + https://github.com/DarthAffe/ScreenCapture.NET + True + + + + + 3.0.0 + 3.0.0 + 3.0.0 + + ..\bin\ + true + True + True + snupkg + + + + $(DefineConstants);TRACE;DEBUG + true + full + false + + + + portable + true + $(NoWarn);CS1591;CS1572;CS1573 + $(DefineConstants);RELEASE + + + + + + + + + diff --git a/ScreenCapture.NET.SCK/ScreenCaptureDelegate.cs b/ScreenCapture.NET.SCK/ScreenCaptureDelegate.cs new file mode 100644 index 0000000..b8610b1 --- /dev/null +++ b/ScreenCapture.NET.SCK/ScreenCaptureDelegate.cs @@ -0,0 +1,113 @@ +using System; +using System.Runtime.InteropServices; +using AudioToolbox; +using CoreMedia; +using CoreVideo; +using ObjCRuntime; +using ScreenCaptureKit; +using Serilog; + +namespace ScreenCapture.NET.SCK; + +public class ScreenCaptureDelegate : NSObject, ISCStreamOutput, INativeObject, IDisposable, ISCStreamDelegate +{ + public ScreenCaptureDelegate(byte[] buffer) + { + _buffer = buffer; + } + public event Action StreamStopped; + public int BufferReceived { get; set; } + private AudioBuffer[] _audioBuffer; + private byte[] _buffer; + [Export("init")] + public ScreenCaptureDelegate() + { + + } + [Foundation.Export("stream:didOutputSampleBuffer:ofType:")] + public unsafe void DidOutputSampleBuffer(SCStream stream, CMSampleBuffer sampleBuffer, SCStreamOutputType type) + { + + try + { + if (type == SCStreamOutputType.Screen) + { + // Process video frame + using (sampleBuffer) + { + var imageBuffer = sampleBuffer.GetImageBuffer() as CVPixelBuffer; + if (imageBuffer != null) + { + using (imageBuffer) + { + imageBuffer.Lock(lockFlags: CVPixelBufferLock.ReadOnly); + IntPtr baseAddress = imageBuffer.BaseAddress; + int bytesPerRow = (int)imageBuffer.BytesPerRow; + int width = (int)imageBuffer.Width; + int height = (int)imageBuffer.Height; + + Marshal.Copy(baseAddress, _buffer, 0, _buffer.Length); + imageBuffer.Unlock(CVPixelBufferLock.ReadOnly); + } + } + } + + } + else if (type == SCStreamOutputType.Audio) + { + // Process audio buffer + var formatDescription = sampleBuffer.GetAudioFormatDescription(); + var numSamples = (int)sampleBuffer.NumSamples; + _audioBuffer = new AudioBuffer[numSamples]; + AudioBuffers outputBuffer = new AudioBuffers(numSamples); // Replace '1' with the appropriate number of buffers required + var error = sampleBuffer.CopyPCMDataIntoAudioBufferList(0, (int)sampleBuffer.NumSamples, outputBuffer); + + if (error != CMSampleBufferError.None) + { + Log.Error(error.ToString()); + } + else + { + ProcessAudioBuffer(outputBuffer); + outputBuffer.Dispose(); + sampleBuffer.Dispose(); + } + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + + } + + private unsafe void ProcessAudioBuffer(AudioBuffers audioBufferList) + { + for (int i = 0; i < audioBufferList.Count; i++) + { + + var audioBuffer = audioBufferList[i]; + _audioBuffer[i] = audioBuffer; + + // IntPtr audioData = audioBufferList[i].Data; + // int audioDataByteSize = audioBufferList[i].DataByteSize; + + // // Convert audio data to a float array for processing + // float[] audioSamples = new float[audioDataByteSize / sizeof(float)]; + // Marshal.Copy(audioData, audioSamples, 0, audioSamples.Length); + + // // Perform further processing on the audioSamples array (e.g., FFT, visualization, etc.) + // Console.WriteLine($"Processed {audioSamples.Length} audio samples."); + } + + } + [Export("stream:didStopWithError:")] + void DidStop(SCStream stream, NSError error) + { + if (error != null) + { + Log.Error("Error while capturing screen: " + error.ToString()); + StreamStopped?.Invoke(); + } + } +} diff --git a/ScreenCapture.NET.sln b/ScreenCapture.NET.sln index b91b878..6901959 100644 --- a/ScreenCapture.NET.sln +++ b/ScreenCapture.NET.sln @@ -15,32 +15,90 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX9", "Sc EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.X11", "ScreenCapture.NET.X11\ScreenCapture.NET.X11.csproj", "{F81562C8-2035-4FB9-9547-C51F9D343BDF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenCapture.NET.SCK", "ScreenCapture.NET.SCK\ScreenCapture.NET.SCK.csproj", "{102F2DAB-C4AC-40E3-A4ED-66851D98917F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {90596344-E012-4534-A933-3BD1B55469DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {90596344-E012-4534-A933-3BD1B55469DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90596344-E012-4534-A933-3BD1B55469DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {90596344-E012-4534-A933-3BD1B55469DC}.Debug|x64.Build.0 = Debug|Any CPU + {90596344-E012-4534-A933-3BD1B55469DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {90596344-E012-4534-A933-3BD1B55469DC}.Debug|x86.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 + {90596344-E012-4534-A933-3BD1B55469DC}.Release|x64.ActiveCfg = Release|Any CPU + {90596344-E012-4534-A933-3BD1B55469DC}.Release|x64.Build.0 = Release|Any CPU + {90596344-E012-4534-A933-3BD1B55469DC}.Release|x86.ActiveCfg = Release|Any CPU + {90596344-E012-4534-A933-3BD1B55469DC}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU + {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Debug|x64.Build.0 = Debug|Any CPU + {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Debug|x86.ActiveCfg = Debug|Any CPU + {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Debug|x86.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 + {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|x64.ActiveCfg = Release|Any CPU + {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|x64.Build.0 = Release|Any CPU + {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|x86.ActiveCfg = Release|Any CPU + {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|x64.Build.0 = Debug|Any CPU + {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|x86.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 + {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|x64.ActiveCfg = Release|Any CPU + {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|x64.Build.0 = Release|Any CPU + {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|x86.ActiveCfg = Release|Any CPU + {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|x64.Build.0 = Debug|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|x86.ActiveCfg = Debug|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|x86.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 + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|x64.ActiveCfg = Release|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|x64.Build.0 = Release|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|x86.ActiveCfg = Release|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU + {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Debug|x64.Build.0 = Debug|Any CPU + {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Debug|x86.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 + {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|x64.ActiveCfg = Release|Any CPU + {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|x64.Build.0 = Release|Any CPU + {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|x86.ActiveCfg = Release|Any CPU + {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|x86.Build.0 = Release|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Debug|x64.ActiveCfg = Debug|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Debug|x64.Build.0 = Debug|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Debug|x86.ActiveCfg = Debug|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Debug|x86.Build.0 = Debug|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Release|Any CPU.Build.0 = Release|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Release|x64.ActiveCfg = Release|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Release|x64.Build.0 = Release|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Release|x86.ActiveCfg = Release|Any CPU + {102F2DAB-C4AC-40E3-A4ED-66851D98917F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tests/.DS_Store b/Tests/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d290c16a194975958b2ac99db2212bc6e29fc245 GIT binary patch literal 6148 zcmeHKJx>EM41Ir^1iGOe^?FbC*4*;&+supqWo}bS1M?oiUU_w=8SIKi6 z+ex0(~F$)La1`&c2Z~+7?NQe%IJxAyHXQ42o3O!5q7dy4uzM-i_ zM7Ph&Mx+;!1>7iW3nNqHot)$<_sipaI}O9lN^Y~H72v&$_H&z{0#twsPys4H1tz3G z9^{MVgr13yLItS6G!(G!LxCG>vIYIqf#4$mI7itHYo8^+Vg;}!TM!kPMk^St>SKu2 zy&WuhT}`%Nw2S8Op?PPuDF&v|E?SVlv^p5502LT1&_&+c`M-yMoBu~GOsN1B_%j7` zzB}x;c&R*FKVHx3$E@1A!9l+q;q4~?i5 Date: Sat, 14 Mar 2026 13:40:08 +0700 Subject: [PATCH 2/2] Remove .DS_Store files and ignore them --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 3 +++ 2 files changed, 3 insertions(+) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 302d408f28d3427749680fd22bd6bbca577c76ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMTWl0n7(U;$(3yeKQ)o-+2w7>utp&@a<*IIPPy_<9+Y1F;cXtMuFr6tov)dve zR*f%!MiY%s;wAbhi7^^ugy0+T!3R@OG%+S7zUYHNA2s@)IkUhPcrYY}ggMDM|NQ4a z|G9kU`{(SLWsISxXs%_fnK7nP7Lw1Q;tomJMZPU5QB5^TkUwL7Zo(hWIbQCzwsA#_ zK#V|)K#V|)K#aisfdHM^BB^&d_oX#1V+3LZ9!Lbl`yoMD$Ydm^qzqpjRD@RmlHv*w zCVHlOKG&dWO$0(X{uD2e#+>Lsr2jnc;OlEpKPubN3Z2VIR(W zPI1ijGrT^Zb@D|g8(7_rld_+&{E@(#48+r@?-hdHu;GYJzM?L}dC|A_TjXGutUhw{ z8)i%!+J(7|i*(C_c;CS8ebcq`>bR~iXc#WpzMXTd16e+bU>UJtos`cFdaQO$(bBwHStMbJM5X%SbV2pD5bFN#3fe(|lfC?GhuQA2Ml9 zdpuM0W~%&SEwr>63FDx!^cE~C&B$T7u~JX_v@DfpWy5xTjh;SEo2zkO)~wan8-q?Z zXE_=-tCL2L@st>Och(Q~j$5vn)fH*$ zC)=ohEL*VlP7uGMN$sTCaL1@#IQ5%$;Dts4WKY5{4v6l$O*33ZbJId@>^IUX|5~NZ zvE7nQoTg2e)K*)QtK|NKptdGEq^8j+wTP{uGiiVwqL>+HC)qjnCOgkQV4tu{>;eA}dMO?yVe1R|V4X)x_{DkZH1ApRgrAAQ|O_{GOP#Tpc zWu>x8S+8tTy5-cBa=eUI5>M$N{gyWX#Y^-BPdRa?cxlbQntdLyOC?b2FM`bPW>Ytw~kTme)kB zA6}{w_?=kNkvB)YzWSMeHN$9cR(#CQn8dOxQ^!@rP?xiD;;9(o)D;#-p%6v-+%Y-|MxUT>>MKyBXEBs zfa<