From ca7454e1eaa1e31f93441e17480b02df656e45ef Mon Sep 17 00:00:00 2001 From: Carti Date: Thu, 12 Feb 2026 09:14:17 +0100 Subject: [PATCH] New Video Format - Squashed & memory management Co-authored-by: Felipe Quintella --- FlashCap.Core/CaptureDevice.cs | 6 +- FlashCap.Core/Devices/AVFoundationDevice.cs | 142 +++++++--- FlashCap.Core/FlashCap.Core.csproj | 1 + .../Internal/AVFoundation/AVCaptureSession.cs | 170 ++++++++---- .../AVFoundation/AVCaptureVideoDataOutput.cs | 242 ++++++++++++++---- .../Internal/AVFoundation/LibAVFoundation.cs | 51 +++- FlashCap.Core/Internal/NativeMethods.cs | 4 + .../Internal/NativeMethods_AVFoundation.cs | 39 ++- FlashCap.Core/VideoCharacteristics.cs | 3 +- 9 files changed, 497 insertions(+), 161 deletions(-) diff --git a/FlashCap.Core/CaptureDevice.cs b/FlashCap.Core/CaptureDevice.cs index 1dbca6b..7508590 100644 --- a/FlashCap.Core/CaptureDevice.cs +++ b/FlashCap.Core/CaptureDevice.cs @@ -33,7 +33,7 @@ protected CaptureDevice(object identity, string name) [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif public void Dispose() => - _ = this.DisposeAsync().ConfigureAwait(false); + _ = this.DisposeAsync().ConfigureAwait(true); #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1 ValueTask IAsyncDisposable.DisposeAsync() => @@ -43,10 +43,10 @@ ValueTask IAsyncDisposable.DisposeAsync() => public async Task DisposeAsync() { using var _ = await locker.LockAsync(default). - ConfigureAwait(false); + ConfigureAwait(true); await this.OnDisposeAsync(). - ConfigureAwait(false); + ConfigureAwait(true); } protected virtual Task OnDisposeAsync() => diff --git a/FlashCap.Core/Devices/AVFoundationDevice.cs b/FlashCap.Core/Devices/AVFoundationDevice.cs index 19a5b38..f3ed7f4 100644 --- a/FlashCap.Core/Devices/AVFoundationDevice.cs +++ b/FlashCap.Core/Devices/AVFoundationDevice.cs @@ -34,6 +34,9 @@ public sealed class AVFoundationDevice : CaptureDevice private AVCaptureSession? session; private FrameProcessor? frameProcessor; private IntPtr bitmapHeader; + private VideoBufferHandler? videoBufferHandler; + + private GCHandle? videoBufferHandlerHandle; public AVFoundationDevice(string uniqueID, string modelID) : base(uniqueID, modelID) @@ -42,39 +45,50 @@ public AVFoundationDevice(string uniqueID, string modelID) : } protected override async Task OnDisposeAsync() - { - // Ensure that we stop the session if it's running - if (this.session is not null && IsRunning) - { - this.session.StopRunning(); - IsRunning = false; - } + { + try + { + // Ensure that we stop the session if it's running + if (session != null && IsRunning) + { + session.StopRunning(); + IsRunning = false; + } + + // Clean up the video buffer handler if it exists + if (videoBufferHandlerHandle.HasValue) + { + videoBufferHandlerHandle.Value.Free(); + videoBufferHandlerHandle = null; + } + + // Now dispose of the session and other resources + if (session != null) + { + session.Dispose(); + session = null; + } + device?.Dispose(); device = null; + deviceInput?.Dispose(); deviceInput = null; + deviceOutput?.Dispose(); deviceOutput = null; + queue?.Dispose(); queue = null; - this.session?.Dispose(); - this.deviceInput?.Dispose(); - this.deviceOutput?.Dispose(); - this.device?.Dispose(); - this.queue?.Dispose(); - - this.session = null; - this.deviceInput = null; - this.deviceOutput = null; - this.device = null; - this.queue = null; - - if (this.bitmapHeader != IntPtr.Zero) - { - Marshal.FreeHGlobal(this.bitmapHeader); - this.bitmapHeader = IntPtr.Zero; - } + if (bitmapHeader != IntPtr.Zero) + { + NativeMethods.FreeMemory(bitmapHeader); + bitmapHeader = IntPtr.Zero; + } - if (this.frameProcessor is not null) + if (frameProcessor != null) + { + await frameProcessor.DisposeAsync().ConfigureAwait(false); + frameProcessor = null; + } + } + finally { - await this.frameProcessor.DisposeAsync().ConfigureAwait(false); - this.frameProcessor = null; + await base.OnDisposeAsync().ConfigureAwait(false); } - - await base.OnDisposeAsync().ConfigureAwait(false); } protected override Task OnInitializeAsync(VideoCharacteristics characteristics, TranscodeFormats transcodeFormat, @@ -142,14 +156,21 @@ format.FormatDescription.Dimensions is var dimensions && { var validPixelFormat = this.deviceOutput.AvailableVideoCVPixelFormatTypes.FirstOrDefault(p => p == pixelFormatType); this.deviceOutput.SetPixelFormatType(validPixelFormat); + this.deviceOutput.SetVideoOutputSize(characteristics.Width, characteristics.Height, validPixelFormat); } else { // Fallback to the mapped pixel format if no available list is provided this.deviceOutput.SetPixelFormatType(pixelFormatType); } - - this.deviceOutput.SetSampleBufferDelegate(new VideoBufferHandler(this), this.queue); + + videoBufferHandler = new VideoBufferHandler(this); + + this.deviceOutput.SetSampleBufferDelegate(videoBufferHandler, this.queue); + + // Protect against GC moving the delegate + videoBufferHandlerHandle = GCHandle.Alloc(videoBufferHandler); + this.deviceOutput.AlwaysDiscardsLateVideoFrames = true; } finally @@ -174,31 +195,80 @@ format.FormatDescription.Dimensions is var dimensions && catch { NativeMethods.FreeMemory(this.bitmapHeader); + this.bitmapHeader = IntPtr.Zero; + + this.queue?.Dispose(); + this.queue = null; + this.device?.Dispose(); + this.device = null; + this.deviceInput?.Dispose(); + this.deviceInput = null; + this.deviceOutput?.Dispose(); + this.deviceOutput = null; + throw; } } protected override Task OnStartAsync(CancellationToken ct) { - this.session?.StartRunning(); - return TaskCompat.CompletedTask; + try + { + if(session== null) + throw new InvalidOperationException("Session is null"); + this.session?.StartRunning(); + this.IsRunning = true; + return TaskCompat.CompletedTask; + }catch (Exception ex) + { + Debug.WriteLine($"Error starting session: {ex.Message}"); + throw new InvalidOperationException("Failed to start the capture session.", ex); + } + } protected override Task OnStopAsync(CancellationToken ct) { - this.session?.StopRunning(); + try + { + if(session== null) + throw new InvalidOperationException("Session is null"); + if (this.IsRunning) + { + this.session?.StopRunning(); + this.IsRunning = false; + + } + + }catch (Exception ex) + { + Debug.WriteLine($"Error stopping session: {ex.Message}"); + throw new InvalidOperationException("Failed to stop the capture session.", ex); + } return TaskCompat.CompletedTask; } protected override void OnCapture(IntPtr pData, int size, long timestampMicroseconds, long frameIndex, PixelBuffer buffer) { - buffer.CopyIn(this.bitmapHeader, pData, size, timestampMicroseconds, frameIndex, TranscodeFormats.Auto); + try + { + if (this.bitmapHeader == IntPtr.Zero) return; + if (pData == IntPtr.Zero || size <= 0) + { + throw new ArgumentException("Invalid pixel data or size."); + } + buffer.CopyIn(this.bitmapHeader, pData, size, timestampMicroseconds, frameIndex, TranscodeFormats.Auto); + }catch (Exception ex) + { + Debug.WriteLine($"Error capturing frame: {ex.Message}"); + throw new InvalidOperationException("Failed to capture frame.", ex); + } } internal sealed class VideoBufferHandler : AVCaptureVideoDataOutputSampleBuffer { private readonly AVFoundationDevice device; - private int frameIndex; + private int frameIndex = 0; public VideoBufferHandler(AVFoundationDevice device) { diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj index 36d91d6..89648ab 100644 --- a/FlashCap.Core/FlashCap.Core.csproj +++ b/FlashCap.Core/FlashCap.Core.csproj @@ -5,6 +5,7 @@ True $(NoWarn);CS0649 true + 1.0.0 diff --git a/FlashCap.Core/Internal/AVFoundation/AVCaptureSession.cs b/FlashCap.Core/Internal/AVFoundation/AVCaptureSession.cs index d95d85d..2c11b55 100644 --- a/FlashCap.Core/Internal/AVFoundation/AVCaptureSession.cs +++ b/FlashCap.Core/Internal/AVFoundation/AVCaptureSession.cs @@ -9,6 +9,7 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Runtime.InteropServices; using static FlashCap.Internal.NativeMethods_AVFoundation; namespace FlashCap.Internal.AVFoundation; @@ -17,8 +18,9 @@ partial class LibAVFoundation { public sealed class AVCaptureSession : LibObjC.NSObject { + private AVCaptureInput? _videoDataInput; - private AVCaptureVideoDataOutput? _videoDataOutput; + private AVCaptureVideoDataOutput? _videoDataOutput; public AVCaptureSession() : base(IntPtr.Zero, false) { @@ -35,88 +37,152 @@ private void ValidateHandle(string method) private void Init() { - var sessionClass = LibObjC.SendAndGetHandle( - LibObjC.GetClass("AVCaptureSession"), - LibObjC.GetSelector(LibObjC.AllocSelector)); + try + { + var sessionClass = LibObjC.SendAndGetHandle( + LibObjC.GetClass("AVCaptureSession"), + LibObjC.GetSelector(LibObjC.AllocSelector)); - var sessionObj = LibObjC.SendAndGetHandle( - sessionClass, - LibObjC.GetSelector("init")); + var sessionObj = LibObjC.SendAndGetHandle( + sessionClass, + LibObjC.GetSelector("init")); - Handle = sessionObj; - ValidateHandle(nameof(Init)); + Handle = sessionObj; + ValidateHandle(nameof(Init)); - LibCoreFoundation.CFRetain(this.Handle); + LibCoreFoundation.CFRetain(this.Handle); + + } catch (Exception ex) + { + Console.WriteLine($"Error initializing AVCaptureSession: {ex.Message}"); + throw; + } } public void AddInput(AVCaptureInput input) { - ValidateHandle(nameof(AddInput)); - - if (_videoDataInput is not null) + try { - throw new InvalidOperationException("Only one video data input can be added to the session."); - } - - _videoDataInput = input; + ValidateHandle(nameof(AddInput)); - LibObjC.SendNoResult( - Handle, - LibObjC.GetSelector("addInput:"), - input.Handle); + _videoDataInput = input as AVCaptureInput; + + LibObjC.SendNoResult( + Handle, + LibObjC.GetSelector("addInput:"), + input.Handle); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling addInput: {ex.Message}"); + throw; + } } - public void AddOutput(AVCaptureVideoDataOutput output) - { - ValidateHandle(nameof(AddOutput)); - if (_videoDataOutput is not null) + public void AddOutput(AVCaptureOutput output) + { + try { - throw new InvalidOperationException("Only one video data output can be added to the session."); - } + _videoDataOutput = output as AVCaptureVideoDataOutput ; - _videoDataOutput = output; + if (_videoDataOutput == null) + { + throw new Exception("Failed to get video data output"); + } + + var videoDataOutput = _videoDataOutput.Handle; - LibObjC.SendNoResult( - Handle, - LibObjC.GetSelector("addOutput:"), - output.Handle); + ValidateHandle(nameof(AddOutput)); + LibObjC.SendNoResult( + Handle, + LibObjC.GetSelector("addOutput:"), + videoDataOutput); + + } + catch (Exception ex) + { + Console.WriteLine($"Error calling addOutput: {ex.Message}"); + throw; + } } public bool CanAddOutput(AVCaptureOutput output) { - ValidateHandle(nameof(CanAddOutput)); - return LibObjC.SendAndGetBool( - Handle, - LibObjC.GetSelector("canAddOutput:"), - output.Handle); + try + { + ValidateHandle(nameof(CanAddOutput)); + return LibObjC.SendAndGetBool( + Handle, + LibObjC.GetSelector("canAddOutput:"), + output.Handle); + } + catch (Exception ex) + { + Console.WriteLine($"Error calling canAddOutput: {ex.Message}"); + throw; + } } + public void StartRunning() { - ValidateHandle(nameof(StartRunning)); - LibObjC.SendNoResult( - Handle, - LibObjC.GetSelector("startRunning")); + try + { + ValidateHandle(nameof(StartRunning)); + LibObjC.SendNoResult( + Handle, + LibObjC.GetSelector("startRunning")); + } + catch (Exception ex) + { + Console.WriteLine($"Error starting AVCaptureSession: {ex.Message}"); + } } public void StopRunning() { - ValidateHandle(nameof(StopRunning)); - LibObjC.SendNoResult( - Handle, - LibObjC.GetSelector("stopRunning")); + try + { + ValidateHandle(nameof(StopRunning)); + var selector = LibObjC.GetSelector("stopRunning"); + LibObjC.SendNoResult( + Handle, + selector); + } catch (Exception ex) + { + Console.WriteLine($"Error stopping AVCaptureSession: {ex.Message}"); + } + } protected override void Dispose(bool disposing) { - _videoDataOutput?.Dispose(); - _videoDataOutput = null; - - _videoDataInput?.Dispose(); - _videoDataInput = null; - - base.Dispose(disposing); + try + { + /*if (Handle != IntPtr.Zero) + { + LibCoreFoundation.CFRelease(Handle); + Handle = IntPtr.Zero; + }*/ + + if (_videoDataOutput != null) + { + _videoDataOutput.Dispose(); + _videoDataOutput = null; + } + if (_videoDataInput != null) + { + _videoDataInput.Dispose(); + _videoDataInput = null; + } + + base.Dispose(disposing); + } catch (Exception ex) + { + Console.WriteLine($"Error disposing AVCaptureSession: {ex.Message}"); + } } + } } diff --git a/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs b/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs index 534851e..1fd59fc 100644 --- a/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs +++ b/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs @@ -21,6 +21,7 @@ partial class LibAVFoundation public sealed class AVCaptureVideoDataOutput : AVCaptureOutput { private AVCaptureVideoDataOutputSampleBuffer.CaptureOutputDidOutputSampleBuffer? callbackDelegate; + private GCHandle? callbackHandle; public AVCaptureVideoDataOutput() : base(IntPtr.Zero, retain: false) { @@ -39,90 +40,231 @@ private void Init() LibCoreFoundation.CFRetain(Handle); } + + private void ValidateHandle() + { + if (Handle == IntPtr.Zero) + { + throw new ObjectDisposedException(nameof(AVCaptureVideoDataOutput), "Handle invalid. 0H01X"); + } + } - public unsafe int[] AvailableVideoCVPixelFormatTypes => - LibCoreFoundation.CFArray.ToArray( - LibObjC.SendAndGetHandle( - Handle, - LibObjC.GetSelector("availableVideoCVPixelFormatTypes")), - static handle => - { - int value; - if (LibCoreFoundation.CFNumberGetValue(handle, LibCoreFoundation.CFNumberType.sInt32Type, &value)) - return value; - throw new InvalidOperationException("The value contained by CFNumber cannot be read as 32-bit signed integer."); - }); + public unsafe int[] AvailableVideoCVPixelFormatTypes + { + get + { + ValidateHandle(); + return LibCoreFoundation.CFArray.ToArray( + LibObjC.SendAndGetHandle( + Handle, + LibObjC.GetSelector("availableVideoCVPixelFormatTypes")), + static handle => + { + int value; + if (LibCoreFoundation.CFNumberGetValue(handle, LibCoreFoundation.CFNumberType.sInt32Type, &value)) + return value; + throw new InvalidOperationException("The value contained by CFNumber cannot be read as 32-bit signed integer."); + }); + } + } public bool AlwaysDiscardsLateVideoFrames { - get => - LibObjC.SendAndGetBool( + get + { + ValidateHandle(); + return LibObjC.SendAndGetBool( Handle, LibObjC.GetSelector("alwaysDiscardsLateVideoFrames")); - set => + } + set + { + ValidateHandle(); LibObjC.SendNoResult( Handle, LibObjC.GetSelector("setAlwaysDiscardsLateVideoFrames:"), value); + } } + public void SetPixelFormatType(int format) { - var pixelFormat = format; - - IntPtr pixelFormatTypeKeyPtr = Dlfcn.dlsym(LibCoreVideo.Handle, "kCVPixelBufferPixelFormatTypeKey"); - if (pixelFormatTypeKeyPtr == IntPtr.Zero) + try + { + ValidateHandle(); + var pixelFormat = format; + + IntPtr pixelFormatTypeKeyPtr = Dlfcn.dlsym(LibCoreVideo.Handle, "kCVPixelBufferPixelFormatTypeKey"); + if (pixelFormatTypeKeyPtr == IntPtr.Zero) + { + throw new Exception("Error comunicating with the AVCaptureVideoDataOutput"); + } + + IntPtr nsPixelFormatKey = Marshal.ReadIntPtr(pixelFormatTypeKeyPtr); + IntPtr nsNumber = LibObjC.CreateNSNumber(pixelFormat); + + IntPtr nsDictionaryClass = LibObjC.GetClass("NSDictionary"); + IntPtr dictSel = LibObjC.GetSelector("dictionaryWithObject:forKey:"); + IntPtr videoSettings = LibObjC.SendAndGetHandle(nsDictionaryClass, dictSel, nsNumber, nsPixelFormatKey); + IntPtr setVideoSettingsSel = LibObjC.GetSelector("setVideoSettings:"); + LibObjC.SendNoResult(this.Handle, setVideoSettingsSel, videoSettings); + } + catch (Exception ex) { - throw new Exception("Error communicating with the AVCaptureVideoDataOutput"); + Debug.WriteLine($"Error setting pixel format type: {ex.Message}"); + throw; } + + } - // Get NSString value - IntPtr nsPixelFormatKey = Marshal.ReadIntPtr(pixelFormatTypeKeyPtr); - IntPtr nsNumber = LibObjC.CreateNSNumber(pixelFormat); + public void SetVideoOutputSize(int width, int height, int pixelFormat) + { + ValidateHandle(); + + // Cria NSNumber para pixelFormat + IntPtr nsPixelFormatKeyPtr = Dlfcn.dlsym(LibCoreVideo.Handle, "kCVPixelBufferPixelFormatTypeKey"); + if (nsPixelFormatKeyPtr == IntPtr.Zero) + throw new Exception("Error comunicating with the AVCaptureVideoDataOutput"); + IntPtr nsPixelFormatKey = Marshal.ReadIntPtr(nsPixelFormatKeyPtr); + IntPtr nsNumberPixelFormat = LibObjC.CreateNSNumber(pixelFormat); + + // Cria NSNumber para width + IntPtr nsWidthKeyPtr = Dlfcn.dlsym(LibCoreVideo.Handle, "kCVPixelBufferWidthKey"); + if (nsWidthKeyPtr == IntPtr.Zero) + throw new Exception("Error comunicating with the AVCaptureVideoDataOutput"); + IntPtr nsWidthKey = Marshal.ReadIntPtr(nsWidthKeyPtr); + IntPtr nsNumberWidth = LibObjC.CreateNSNumber(width); + + // Cria NSNumber para height + IntPtr nsHeightKeyPtr = Dlfcn.dlsym(LibCoreVideo.Handle, "kCVPixelBufferHeightKey"); + if (nsHeightKeyPtr == IntPtr.Zero) + throw new Exception("Error comunicating with the AVCaptureVideoDataOutput"); + IntPtr nsHeightKey = Marshal.ReadIntPtr(nsHeightKeyPtr); + IntPtr nsNumberHeight = LibObjC.CreateNSNumber(height); + // Cria NSArray de keys e values + IntPtr nsArrayClass = LibObjC.GetClass("NSArray"); + IntPtr arrayWithObjectsSel = LibObjC.GetSelector("arrayWithObjects:count:"); + IntPtr keysArray; + IntPtr valuesArray; + unsafe + { + IntPtr* keys = stackalloc IntPtr[3] { nsPixelFormatKey, nsWidthKey, nsHeightKey }; + IntPtr* values = stackalloc IntPtr[3] { nsNumberPixelFormat, nsNumberWidth, nsNumberHeight }; + + keysArray = LibObjC.SendAndGetHandle(nsArrayClass, arrayWithObjectsSel, (IntPtr)keys, new IntPtr(3)); + valuesArray = LibObjC.SendAndGetHandle(nsArrayClass, arrayWithObjectsSel, (IntPtr)values, new IntPtr(3)); + } + + // Cria NSDictionary com as chaves e valores IntPtr nsDictionaryClass = LibObjC.GetClass("NSDictionary"); - IntPtr dictSel = LibObjC.GetSelector("dictionaryWithObject:forKey:"); - IntPtr videoSettings = LibObjC.SendAndGetHandle(nsDictionaryClass, dictSel, nsNumber, nsPixelFormatKey); + IntPtr dictWithObjectsForKeysSel = LibObjC.GetSelector("dictionaryWithObjects:forKeys:"); + IntPtr videoSettings = LibObjC.SendAndGetHandle(nsDictionaryClass, dictWithObjectsForKeysSel, valuesArray, keysArray); + + // Seta o dicionĂ¡rio como video settings IntPtr setVideoSettingsSel = LibObjC.GetSelector("setVideoSettings:"); LibObjC.SendNoResult(this.Handle, setVideoSettingsSel, videoSettings); - } public void SetSampleBufferDelegate(AVFoundationDevice.VideoBufferHandler sampleBufferDelegate, LibCoreFoundation.DispatchQueue sampleBufferCallbackQueue) { - if (sampleBufferDelegate == null) + try + { + ValidateHandle(); + if (sampleBufferDelegate == null) + { + Debug.WriteLine("AVCaptureVideoDataOutputSampleBufferDelegate is null"); + return; + } + + // If the handle is already set, free the previous delegate + if (callbackHandle.HasValue) + { + callbackHandle.Value.Free(); + callbackHandle = null; + } + + IntPtr selDidOutput = LibObjC.GetSelector("captureOutput:didOutputSampleBuffer:fromConnection:"); + callbackDelegate = sampleBufferDelegate.CaptureOutputCallback; + callbackHandle = GCHandle.Alloc(callbackDelegate); + IntPtr impCallback = Marshal.GetFunctionPointerForDelegate(callbackDelegate); + + IntPtr allocSel = LibObjC.GetSelector("alloc"); + IntPtr initSel = LibObjC.GetSelector("init"); + IntPtr nsObjectClass = LibObjC.GetClass("NSObject"); + IntPtr delegateClass = + LibObjC.objc_allocateClassPair(nsObjectClass, "CaptureDelegate_" + Handle, IntPtr.Zero); + + string types = "v@:@@@"; + bool added = LibObjC.class_addMethod(delegateClass, selDidOutput, impCallback, types); + if (!added) + { + return; + } + + LibObjC.objc_registerClassPair(delegateClass); + + IntPtr delegateInstanceAlloc = LibObjC.SendAndGetHandle(delegateClass, allocSel); + IntPtr delegateInstance = LibObjC.SendAndGetHandle(delegateInstanceAlloc, initSel); + + IntPtr setDelegateSel = LibObjC.GetSelector("setSampleBufferDelegate:queue:"); + LibObjC.SendNoResult(Handle, setDelegateSel, delegateInstance, sampleBufferCallbackQueue.Handle); + } + catch (Exception ex) { - Debug.WriteLine("AVCaptureVideoDataOutputSampleBufferDelegate is null"); - return; + Debug.WriteLine($"Error setting sample buffer: {ex.Message}"); + throw new InvalidOperationException("Failed to set sample buffer delegate.", ex); } - IntPtr allocSel = LibObjC.GetSelector("alloc"); - IntPtr initSel = LibObjC.GetSelector("init"); - IntPtr nsObjectClass = LibObjC.GetClass("NSObject"); - IntPtr delegateClass = LibObjC.objc_allocateClassPair(nsObjectClass, "CaptureDelegate_" + Handle, IntPtr.Zero); - IntPtr selDidOutput = LibObjC.GetSelector("captureOutput:didOutputSampleBuffer:fromConnection:"); - - callbackDelegate = sampleBufferDelegate.CaptureOutputCallback; - - IntPtr impCallback = Marshal.GetFunctionPointerForDelegate(callbackDelegate); + } + + public new void Dispose() + { + try + { + Dispose(true); + GC.SuppressFinalize(this); + } + catch (Exception ex) + { + Debug.WriteLine($"Error disposing AVCaptureVideoDataOutput: {ex.Message}"); + throw new InvalidOperationException("Failed to dispose AVCaptureVideoDataOutput.", ex); + } + + } - // "v@:@@@" this means the method returns void and receives (self, _cmd, output, sampleBuffer, connection). - string types = "v@:@@@"; - bool added = LibObjC.class_addMethod(delegateClass, selDidOutput, impCallback, types); - if (!added) + protected override void Dispose(bool disposing) + { + try + { + // Cleans delegates GCHandle + if (callbackHandle.HasValue) + { + callbackHandle.Value.Free(); + callbackHandle = null; + } + + + if (Handle != IntPtr.Zero) + { + LibCoreFoundation.CFRelease(Handle); + Handle = IntPtr.Zero; + } + + base.Dispose(disposing); + } catch( Exception ex) { - return; + Debug.WriteLine($"Error in Dispose: {ex.Message}"); + throw new InvalidOperationException("Failed to dispose AVCaptureVideoDataOutput.", ex); } - LibObjC.objc_registerClassPair(delegateClass); + } - // Delegate creation - IntPtr delegateInstanceAlloc = LibObjC.SendAndGetHandle(delegateClass, allocSel); - IntPtr delegateInstance = LibObjC.SendAndGetHandle(delegateInstanceAlloc, initSel); - - IntPtr setDelegateSel = LibObjC.GetSelector("setSampleBufferDelegate:queue:"); - LibObjC.SendNoResult(Handle, setDelegateSel, delegateInstance, sampleBufferCallbackQueue.Handle); + ~AVCaptureVideoDataOutput() + { + Dispose(false); } } } diff --git a/FlashCap.Core/Internal/AVFoundation/LibAVFoundation.cs b/FlashCap.Core/Internal/AVFoundation/LibAVFoundation.cs index e23130c..8d5acc3 100644 --- a/FlashCap.Core/Internal/AVFoundation/LibAVFoundation.cs +++ b/FlashCap.Core/Internal/AVFoundation/LibAVFoundation.cs @@ -9,6 +9,7 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Diagnostics; using System.Runtime.InteropServices; using System.Linq; using static FlashCap.Internal.NativeMethods_AVFoundation; @@ -115,31 +116,65 @@ public string LocalizedName } } + + private AVCaptureDeviceFormat[]? _formats; + private AVCaptureDeviceFormat? _activeFormat; public AVCaptureDeviceFormat[] Formats { get { + _formats?.ToList().ForEach(f => f.Dispose()); + var handle = LibObjC.SendAndGetHandle( Handle, LibObjC.GetSelector("formats")); - return LibCoreFoundation.CFArray.ToArray(handle, static handle => new AVCaptureDeviceFormat(handle, retain: true)); + _formats = LibCoreFoundation.CFArray.ToArray(handle, static handle => new AVCaptureDeviceFormat(handle, retain: true)); + + return _formats; } } public AVCaptureDeviceFormat ActiveFormat { - get => new AVCaptureDeviceFormat( - LibObjC.SendAndGetHandle( - Handle, - LibObjC.GetSelector("activeFormat")), - retain: true); - set => + get + { + //_activeFormat?.Dispose(); + if(_activeFormat == null) + _activeFormat = new AVCaptureDeviceFormat( + LibObjC.SendAndGetHandle( + Handle, + LibObjC.GetSelector("activeFormat")), + retain: true); + return _activeFormat; + } + set + { LibObjC.SendNoResult( Handle, LibObjC.GetSelector("setActiveFormat:"), value.Handle); + _activeFormat?.Dispose(); + _activeFormat = value; + } + + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_formats != null) + { + foreach (var format in _formats) + format.Dispose(); + _formats = null; + } + _activeFormat?.Dispose(); + _activeFormat = null; + } + base.Dispose(disposing); } public LibCoreMedia.CMTime ActiveVideoMinFrameDuration @@ -216,7 +251,7 @@ public unsafe void UnlockForConfiguration() LibObjC.GetSelector("deviceWithUniqueID:"), nativeDeviceUniqueID.Handle); - return handle == IntPtr.Zero ? null : new AVCaptureDevice(handle, retain: false); + return handle == IntPtr.Zero ? null : new AVCaptureDevice(handle, retain: true); } public static AVAuthorizationStatus GetAuthorizationStatus(IntPtr mediaType) diff --git a/FlashCap.Core/Internal/NativeMethods.cs b/FlashCap.Core/Internal/NativeMethods.cs index dd65909..494dd70 100644 --- a/FlashCap.Core/Internal/NativeMethods.cs +++ b/FlashCap.Core/Internal/NativeMethods.cs @@ -561,6 +561,10 @@ public static bool GetCompressionAndBitCount( compression = Compression.ARGB; bitCount = 32; return true; + case PixelFormats.BGRA32: + compression = Compression.ARGB; + bitCount = 32; + return true; case PixelFormats.RGB16: compression = Compression.D3D_RGB565; bitCount = 16; diff --git a/FlashCap.Core/Internal/NativeMethods_AVFoundation.cs b/FlashCap.Core/Internal/NativeMethods_AVFoundation.cs index cdef71b..15b57ed 100644 --- a/FlashCap.Core/Internal/NativeMethods_AVFoundation.cs +++ b/FlashCap.Core/Internal/NativeMethods_AVFoundation.cs @@ -25,8 +25,8 @@ internal static class NativeMethods_AVFoundation //[PixelFormats.UYVY] = LibCoreVideo.PixelFormatType_24RGB, [PixelFormats.RGB32] = 32, //[PixelFormats.ARGB32] = LibCoreVideo.PixelFormatType_32BGRA, - //[PixelFormats.BGRA32] = LibCoreVideo.PixelFormatType_32BGRA, - [PixelFormats.ARGB32] = LibCoreVideo.PixelFormatType_32BGRA, + [PixelFormats.BGRA32] = LibCoreVideo.PixelFormatType_32BGRA, + //[PixelFormats.ARGB32] = LibCoreVideo.PixelFormatType_32BGRA, [PixelFormats.RGB24] = LibCoreVideo.PixelFormatType_24RGB, [PixelFormats.UYVY] = LibCoreVideo.PixelFormatType_422YpCbCr8_yuvs, [PixelFormats.YUYV] = LibCoreVideo.PixelFormatType_422YpCbCr8, @@ -394,6 +394,9 @@ public abstract class NSObject : NativeObject { protected NSObject(IntPtr handle, bool retain) { + + if (handle == IntPtr.Zero) return; + Handle = handle; if (retain) @@ -402,12 +405,16 @@ protected NSObject(IntPtr handle, bool retain) protected override void Dispose(bool disposing) { + if (!disposing) return; + if (Handle == IntPtr.Zero) return; - LibObjC.SendNoResult( + LibCoreFoundation.CFRelease(Handle); + + /*LibObjC.SendNoResult( Handle, - LibObjC.GetSelector(LibObjC.ReleaseSelector)); + LibObjC.GetSelector(LibObjC.ReleaseSelector));*/ Handle = IntPtr.Zero; } @@ -607,16 +614,26 @@ public sealed class DispatchQueue : NativeObject { public DispatchQueue(string label) { - //Handle = LibC.DispatchQueueCreate(label, IntPtr.Zero) is var handle && handle != IntPtr.Zero - // ? handle : throw new InvalidOperationException("Cannot create a dispatch queue."); - Handle = LibSystem.dispatch_queue_create(label, IntPtr.Zero); + //Handle = LibSystem.dispatch_queue_create(label, IntPtr.Zero); + Handle = LibC.DispatchQueueCreate(label, IntPtr.Zero); + if (Handle == IntPtr.Zero) + { + throw new InvalidOperationException("Handle invalid 0H003."); + } CFRetain(Handle); } - - - protected override void Dispose(bool disposing) => - LibC.DispatchRelease(Handle); + + + protected override void Dispose(bool disposing) + { + if (Handle == IntPtr.Zero) return; + if (Handle != IntPtr.Zero) + { + LibC.DispatchRelease(Handle); + Handle = IntPtr.Zero; + } + } } } diff --git a/FlashCap.Core/VideoCharacteristics.cs b/FlashCap.Core/VideoCharacteristics.cs index a877b1c..df700e6 100644 --- a/FlashCap.Core/VideoCharacteristics.cs +++ b/FlashCap.Core/VideoCharacteristics.cs @@ -25,7 +25,8 @@ public enum PixelFormats PNG, UYVY, YUYV, - NV12 + NV12, + BGRA32 } public sealed class VideoCharacteristics :