From 109cf15b2814864b4b19eea718fd0b547bf2e3bb Mon Sep 17 00:00:00 2001 From: ldlac Date: Wed, 5 Apr 2023 17:34:59 -0400 Subject: [PATCH 01/13] add DisplayPropertyPage_CaptureFilter --- FlashCap.Core/Devices/DirectShowDevice.cs | 53 +++++++++++++++++++ .../Internal/NativeMethods_DirectShow.cs | 48 +++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 9bb8b8d..3cf17aa 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -312,6 +312,59 @@ protected override Task OnStopAsync(CancellationToken ct) } } + + public void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + if (graphBuilder != null) + { + graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); + DisplayPropertyPage(captureSourceFilter, hwndOwner); + } + } + + private void DisplayPropertyPage(object? filterOrPin, IntPtr hwndOwner) + { + if (filterOrPin == null) + return; + + if (filterOrPin is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) + return; + + string caption = string.Empty; + if (filterOrPin is NativeMethods_DirectShow.IBaseFilter) + { + if (filterOrPin is not NativeMethods_DirectShow.IBaseFilter filter) + return; + + filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo); + + caption = filterInfo.chName; + + if (filterInfo.graph != null) + { + Marshal.ReleaseComObject(filterInfo.graph); + } + } + else if (filterOrPin is NativeMethods_DirectShow.IPin) + { + if (filterOrPin is not NativeMethods_DirectShow.IPin pin) + return; + + pin.QueryPinInfo(out NativeMethods_DirectShow.PIN_INFO pinInfo); + + caption = pinInfo.name; + } + + + pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID); + + object oDevice = filterOrPin; + NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, caption, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero); + + Marshal.FreeCoTaskMem(caGUID.pElems); + Marshal.ReleaseComObject(pProp); + } + #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 3c40c48..081ce5e 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -297,6 +297,54 @@ public struct FILTER_INFO public IFilterGraph graph; } + + [StructLayout(LayoutKind.Sequential)] + public struct DsCAUUID + { + public int cElems; + public IntPtr pElems; + + public Guid[] ToGuidArray() + { + Guid[] retval = new Guid[cElems]; + + for (int i = 0; i < cElems; i++) + { +#pragma warning disable CS0618 + IntPtr ptr = new(pElems.ToInt64() + (Marshal.SizeOf(typeof(Guid)) * i)); + Guid? guid = (Guid?)Marshal.PtrToStructure(ptr, typeof(Guid)); + retval[i] = guid.GetValueOrDefault(); +#pragma warning restore CS0618 + } + + return retval; + } + } + + [DllImport("oleaut32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] + public static extern int OleCreatePropertyFrame( + IntPtr hwndOwner, + int x, + int y, + [MarshalAs(UnmanagedType.LPWStr)] string lpszCaption, + int cObjects, + [MarshalAs(UnmanagedType.Interface, ArraySubType = UnmanagedType.IUnknown)] + ref object ppUnk, + int cPages, + IntPtr lpPageClsID, + int lcid, + int dwReserved, + IntPtr lpvReserved); + + [SuppressUnmanagedCodeSecurity, + Guid("B196B28B-BAB4-101A-B69C-00AA00341D07"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISpecifyPropertyPages + { + [PreserveSig] + int GetPages(out DsCAUUID pPages); + } + [SuppressUnmanagedCodeSecurity] [Guid("56a86895-0ad4-11ce-b03a-0020af0ba770")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] From 272bb857790b5e888c3a9714b0261f589b3b326f Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 10:23:06 -0400 Subject: [PATCH 02/13] . --- FlashCap.Core/Devices/DirectShowDevice.cs | 53 ++++++++++------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 3cf17aa..8448314 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -318,51 +318,46 @@ public void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) if (graphBuilder != null) { graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); - DisplayPropertyPage(captureSourceFilter, hwndOwner); + DisplayPropertyPage_Filter(captureSourceFilter, hwndOwner); } } - private void DisplayPropertyPage(object? filterOrPin, IntPtr hwndOwner) + private void DisplayPropertyPage_Filter(object? obj, IntPtr hwndOwner) { - if (filterOrPin == null) + if (obj == null) return; - if (filterOrPin is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) + if (obj is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) return; - string caption = string.Empty; - if (filterOrPin is NativeMethods_DirectShow.IBaseFilter) - { - if (filterOrPin is not NativeMethods_DirectShow.IBaseFilter filter) - return; - - filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo); - - caption = filterInfo.chName; + if (obj is not NativeMethods_DirectShow.IBaseFilter filter) + return; - if (filterInfo.graph != null) - { - Marshal.ReleaseComObject(filterInfo.graph); - } - } - else if (filterOrPin is NativeMethods_DirectShow.IPin) + if (filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo) < 0) { - if (filterOrPin is not NativeMethods_DirectShow.IPin pin) - return; - - pin.QueryPinInfo(out NativeMethods_DirectShow.PIN_INFO pinInfo); - - caption = pinInfo.name; + throw new ArgumentException( + $"FlashCap: Couldn't query filter info"); } + if (pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID) < 0) + { + throw new ArgumentException( + $"FlashCap: Couldn't get pages"); + } - pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID); - - object oDevice = filterOrPin; - NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, caption, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero); + object oDevice = obj; + if (NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, filterInfo.chName, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero) < 0) + { + throw new ArgumentException( + $"FlashCap: Couldn't create property frame"); + } Marshal.FreeCoTaskMem(caGUID.pElems); Marshal.ReleaseComObject(pProp); + if (filterInfo.graph != null) + { + Marshal.ReleaseComObject(filterInfo.graph); + } } #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP From 510850be900e200a8a101db2dde44e7d3fbda271 Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 22:57:05 -0400 Subject: [PATCH 03/13] testing with brightness --- FlashCap.Core/CaptureDevice.cs | 5 + FlashCap.Core/CaptureDeviceProperties.cs | 45 +++++++++ FlashCap.Core/Devices/DirectShowDevice.cs | 65 +++++++++++-- FlashCap.Core/Devices/DirectShowProperty.cs | 14 +++ FlashCap.Core/Devices/V4L2Device.cs | 41 +++++--- .../Devices/VideoForWindowsDevice.cs | 27 ++++-- .../Internal/NativeMethods_DirectShow.cs | 60 ++++++++++++ FlashCap.sln | 9 +- .../FlashCap.Properties.csproj | 18 ++++ samples/FlashCap.Properties/Program.cs | 94 +++++++++++++++++++ 10 files changed, 352 insertions(+), 26 deletions(-) create mode 100644 FlashCap.Core/CaptureDeviceProperties.cs create mode 100644 FlashCap.Core/Devices/DirectShowProperty.cs create mode 100644 samples/FlashCap.Properties/FlashCap.Properties.csproj create mode 100644 samples/FlashCap.Properties/Program.cs diff --git a/FlashCap.Core/CaptureDevice.cs b/FlashCap.Core/CaptureDevice.cs index f1bf3dd..cac48c3 100644 --- a/FlashCap.Core/CaptureDevice.cs +++ b/FlashCap.Core/CaptureDevice.cs @@ -66,6 +66,11 @@ protected abstract Task OnInitializeAsync( protected abstract Task OnStartAsync(CancellationToken ct); protected abstract Task OnStopAsync(CancellationToken ct); + public abstract void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner); + public abstract int GetPropertyValue(VideoProcessingAmplifierProperty property); + public abstract void SetPropertyValue(VideoProcessingAmplifierProperty property, object? value); + + public CaptureDeviceProperties Properties { get; protected set; } = new CaptureDeviceProperties(); protected abstract void OnCapture( IntPtr pData, int size, long timestampMicroseconds, long frameIndex, PixelBuffer buffer); diff --git a/FlashCap.Core/CaptureDeviceProperties.cs b/FlashCap.Core/CaptureDeviceProperties.cs new file mode 100644 index 0000000..85ead90 --- /dev/null +++ b/FlashCap.Core/CaptureDeviceProperties.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace FlashCap +{ + public enum VideoProcessingAmplifierProperty + { + Brightness, + Contrast, + Hue, + Saturation, + Sharpness, + Gamma, + ColorEnable, + WhiteBalance, + BacklightCompensation, + Gain + } + + public sealed class CaptureDeviceProperties : Dictionary + { + } + + public abstract class CaptureDeviceProperty + { + public VideoProcessingAmplifierProperty Property { get; private set; } + public int Min { get; private set; } + public int Max { get; private set; } + public int Step { get; private set; } + + protected CaptureDeviceProperty(VideoProcessingAmplifierProperty property, int min, int max, int step) + { + Property = property; + Min = min; + Max = max; + Step = step; + } + + internal bool IsPropertyValueValid(object? obj) + { + var value = obj as int?; + + return value != null && value <= Max && value >= Min && value % Step == 0; + } + } +} diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 8448314..1faaaaa 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -73,6 +73,7 @@ [PreserveSig] public int BufferCB( private bool transcodeIfYUV; private FrameProcessor frameProcessor; private NativeMethods_DirectShow.IGraphBuilder? graphBuilder; + private NativeMethods_DirectShow.ICaptureGraphBuilder2? captureGraphBuilder; private SampleGrabberSink? sampleGrabberSink; private IntPtr pBih; @@ -174,7 +175,7 @@ protected override Task OnInitializeAsync( /////////////////////////////// - var captureGraphBuilder = NativeMethods_DirectShow.CreateCaptureGraphBuilder(); + captureGraphBuilder = NativeMethods_DirectShow.CreateCaptureGraphBuilder(); if (captureGraphBuilder.SetFiltergraph(this.graphBuilder) < 0) { throw new ArgumentException( @@ -312,12 +313,64 @@ protected override Task OnStopAsync(CancellationToken ct) } } + public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + { + if (Properties.TryGetValue(property, out CaptureDeviceProperty _) + && graphBuilder != null + && captureGraphBuilder != null) + { + graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); + if (captureSourceFilter != null) + { + captureGraphBuilder.FindInterface(Guid.Empty, Guid.Empty, captureSourceFilter, NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, out object? videoProcAmpObject); + if (videoProcAmpObject != null) + { + var videoProcAmp = (NativeMethods_DirectShow.IAMVideoProcAmp)videoProcAmpObject; + videoProcAmp.Get(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), out int value, out NativeMethods_DirectShow.VideoProcAmpFlags _); + Marshal.ReleaseComObject(videoProcAmpObject); + return value; + } + Marshal.ReleaseComObject(captureGraphBuilder); + } + } + + throw new ArgumentException( + $"FlashCap: Property is not supported by device: Property={property}"); + } - public void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) { - if (graphBuilder != null) + if (Properties.TryGetValue(property, out CaptureDeviceProperty? captureDeviceProperty) + && obj != null + && captureDeviceProperty != null + && captureDeviceProperty.IsPropertyValueValid(obj) + && graphBuilder != null + && captureGraphBuilder != null) { graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); + if (captureSourceFilter != null) + { + captureGraphBuilder.FindInterface(Guid.Empty, Guid.Empty, captureSourceFilter, NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, out object? videoProcAmpObject); + if (videoProcAmpObject != null) + { + var videoProcAmp = (NativeMethods_DirectShow.IAMVideoProcAmp)videoProcAmpObject; + videoProcAmp.Get(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), out int _, out NativeMethods_DirectShow.VideoProcAmpFlags videoProcAmpFlags); + videoProcAmp.Set(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), (int)obj, videoProcAmpFlags); + Marshal.ReleaseComObject(videoProcAmpObject); + } + Marshal.ReleaseComObject(captureGraphBuilder); + } + } + + throw new ArgumentException( + $"FlashCap: Property is not supported by device: Property={property}"); + } + + public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + if (graphBuilder != null + && graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter) >= 0) + { DisplayPropertyPage_Filter(captureSourceFilter, hwndOwner); } } @@ -335,20 +388,20 @@ private void DisplayPropertyPage_Filter(object? obj, IntPtr hwndOwner) if (filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo) < 0) { - throw new ArgumentException( + throw new Exception( $"FlashCap: Couldn't query filter info"); } if (pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID) < 0) { - throw new ArgumentException( + throw new Exception( $"FlashCap: Couldn't get pages"); } object oDevice = obj; if (NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, filterInfo.chName, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero) < 0) { - throw new ArgumentException( + throw new Exception( $"FlashCap: Couldn't create property frame"); } diff --git a/FlashCap.Core/Devices/DirectShowProperty.cs b/FlashCap.Core/Devices/DirectShowProperty.cs new file mode 100644 index 0000000..5b78099 --- /dev/null +++ b/FlashCap.Core/Devices/DirectShowProperty.cs @@ -0,0 +1,14 @@ +using FlashCap.Internal; + +namespace FlashCap.Devices +{ + public sealed class DirectShowProperty : CaptureDeviceProperty + { + public DirectShowProperty(VideoProcessingAmplifierProperty property, int min, int max, int step) : base(property, min, max, step) { } + + internal static NativeMethods_DirectShow.VideoProcAmpProperty FromVideoProcessingAmplifierProperty(VideoProcessingAmplifierProperty property) + { + return (NativeMethods_DirectShow.VideoProcAmpProperty)(int)property; + } + } +} diff --git a/FlashCap.Core/Devices/V4L2Device.cs b/FlashCap.Core/Devices/V4L2Device.cs index 366ec2a..bfd98af 100644 --- a/FlashCap.Core/Devices/V4L2Device.cs +++ b/FlashCap.Core/Devices/V4L2Device.cs @@ -31,10 +31,10 @@ public sealed class V4L2Device : CaptureDevice private FrameProcessor frameProcessor; private long frameIndex; - + private readonly IntPtr[] pBuffers = new IntPtr[BufferCount]; private readonly int[] bufferLength = new int[BufferCount]; - + private int fd; private IntPtr pBih; private Task task; @@ -93,11 +93,11 @@ protected override unsafe Task OnInitializeAsync( fmt_pix.height = (uint)characteristics.Height; fmt_pix.pixelformat = pix_fmt; fmt_pix.field = (uint)v4l2_field.ANY; - + var format = Interop.Create_v4l2_format(); format.type = (uint)v4l2_buf_type.VIDEO_CAPTURE; format.fmt_pix = fmt_pix; - + if (ioctl(fd, Interop.VIDIOC_S_FMT, format) == 0) { applied = true; @@ -128,14 +128,14 @@ protected override unsafe Task OnInitializeAsync( buffer.type = (uint)v4l2_buf_type.VIDEO_CAPTURE; buffer.memory = (uint)v4l2_memory.MMAP; buffer.index = (uint)index; - + if (ioctl(fd, Interop.VIDIOC_QUERYBUF, buffer) < 0) { var code = Marshal.GetLastWin32Error(); throw new ArgumentException( $"FlashCap: Couldn't assign video buffer: Code={code}, DevicePath={this.devicePath}"); } - + if (mmap(IntPtr.Zero, buffer.length, PROT.READ, MAP.SHARED, fd, buffer.m_offset) is { } pBuffer && pBuffer == MAP_FAILED) @@ -178,7 +178,7 @@ protected override unsafe Task OnInitializeAsync( pBih->biWidth = characteristics.Width; pBih->biHeight = characteristics.Height; pBih->biSizeImage = pBih->CalculateImageSize(); - + this.fd = fd; this.pBih = pih; @@ -203,7 +203,7 @@ protected override unsafe Task OnInitializeAsync( this.bufferLength[index] = default; } } - + close(fd); throw; } @@ -234,7 +234,7 @@ await this.frameProcessor.DisposeAsync(). this.fd, Interop.VIDIOC_STREAMOFF, (int)v4l2_buf_type.VIDEO_CAPTURE); } - + write(this.abortwfd, new byte[] { 0x01 }, 1); await this.task. @@ -254,7 +254,7 @@ static bool IsIgnore(int code) => EINVAL => true, _ => false, }; - + var fds = new[] { new pollfd @@ -361,12 +361,12 @@ static bool IsIgnore(int code) => this.bufferLength[index] = default; } } - + close(this.abortrfd); close(this.fd); - + NativeMethods.FreeMemory(this.pBih); - + this.abortrfd = -1; this.fd = -1; this.pBih = IntPtr.Zero; @@ -419,6 +419,21 @@ protected override Task OnStopAsync(CancellationToken ct) return TaskCompat.CompletedTask; } + public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + throw new Exception("not supported for V4l2Device"); + } + #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif diff --git a/FlashCap.Core/Devices/VideoForWindowsDevice.cs b/FlashCap.Core/Devices/VideoForWindowsDevice.cs index 2407142..1b7f541 100644 --- a/FlashCap.Core/Devices/VideoForWindowsDevice.cs +++ b/FlashCap.Core/Devices/VideoForWindowsDevice.cs @@ -135,15 +135,15 @@ protected override unsafe Task OnInitializeAsync( throw; } - /////////////////////////////////////// + /////////////////////////////////////// - this.handle = handle; + this.handle = handle; - // https://stackoverflow.com/questions/4097235/is-it-necessary-to-gchandle-alloc-each-callback-in-a-class - this.thisPin = GCHandle.Alloc(this, GCHandleType.Normal); - this.callback = this.CallbackEntry; + // https://stackoverflow.com/questions/4097235/is-it-necessary-to-gchandle-alloc-each-callback-in-a-class + this.thisPin = GCHandle.Alloc(this, GCHandleType.Normal); + this.callback = this.CallbackEntry; - NativeMethods_VideoForWindows.capSetCallbackFrame(handle, this.callback); + NativeMethods_VideoForWindows.capSetCallbackFrame(handle, this.callback); }, ct); } @@ -279,6 +279,21 @@ protected override Task OnStopAsync(CancellationToken ct) } } + public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + throw new Exception("not supported for V4l2Device"); + } + #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 081ce5e..66fa254 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -622,6 +622,66 @@ [PreserveSig] int get_RegFilterCollection( //////////////////////////////////////////////////////////////////////// + public static class IAMVideoProcAmpHelper + { + public static Guid GUID = new("C6E13360-30AC-11d0-A18C-00A0C9118956"); + } + + [ComImport, System.Security.SuppressUnmanagedCodeSecurity] + [Guid("C6E13360-30AC-11d0-A18C-00A0C9118956")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAMVideoProcAmp + { + [PreserveSig] + int GetRange( + [In] VideoProcAmpProperty Property, + [Out] out int pMin, + [Out] out int pMax, + [Out] out int pSteppingDelta, + [Out] out int pDefault, + [Out] out VideoProcAmpFlags pCapsFlags + ); + + [PreserveSig] + int Set( + [In] VideoProcAmpProperty Property, + [In] int lValue, + [In] VideoProcAmpFlags Flags + ); + + [PreserveSig] + int Get( + [In] VideoProcAmpProperty Property, + [Out] out int lValue, + [Out] out VideoProcAmpFlags Flags + ); + } + + + [Flags] + public enum VideoProcAmpFlags + { + None = 0, + Auto = 0x0001, + Manual = 0x0002 + } + + public enum VideoProcAmpProperty + { + Brightness, + Contrast, + Hue, + Saturation, + Sharpness, + Gamma, + ColorEnable, + WhiteBalance, + BacklightCompensation, + Gain + } + + //////////////////////////////////////////////////////////////////////// + public static readonly Guid CLSID_SystemDeviceEnum = new Guid("62BE5D10-60EB-11d0-BD3B-00A0C911CE86"); public static readonly Guid CLSID_VideoInputDeviceCategory = diff --git a/FlashCap.sln b/FlashCap.sln index 5233699..dffd003 100644 --- a/FlashCap.sln +++ b/FlashCap.sln @@ -35,7 +35,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.Core", "FlashCap.C EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.FlashCap", "FSharp.FlashCap\FSharp.FlashCap.fsproj", "{AAD7F2E9-C6F7-4BDA-9D49-08469C2C7A1D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlashCap.OneShot", "samples\FlashCap.OneShot\FlashCap.OneShot.csproj", "{2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.OneShot", "samples\FlashCap.OneShot\FlashCap.OneShot.csproj", "{2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.Properties", "samples\FlashCap.Properties\FlashCap.Properties.csproj", "{273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -79,6 +81,10 @@ Global {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}.Release|Any CPU.Build.0 = Release|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,6 +96,7 @@ Global {B8B4CFA8-4360-4503-A316-03EAD02AD96E} = {A1BDFB2A-48C0-4D65-AE03-4DBFC12B4BA8} {583903DC-9D94-4A63-AFCB-2FB88CE14C95} = {655486CC-57A5-494E-97D0-9850B6D55BBA} {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE} = {655486CC-57A5-494E-97D0-9850B6D55BBA} + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D} = {655486CC-57A5-494E-97D0-9850B6D55BBA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {53E3C2D2-05FD-4396-ABF1-6B313F8F82A9} diff --git a/samples/FlashCap.Properties/FlashCap.Properties.csproj b/samples/FlashCap.Properties/FlashCap.Properties.csproj new file mode 100644 index 0000000..e6627bd --- /dev/null +++ b/samples/FlashCap.Properties/FlashCap.Properties.csproj @@ -0,0 +1,18 @@ + + + + Exe + net48;net6.0 + latest + Enable + + + + + + + + + + + diff --git a/samples/FlashCap.Properties/Program.cs b/samples/FlashCap.Properties/Program.cs new file mode 100644 index 0000000..57d3638 --- /dev/null +++ b/samples/FlashCap.Properties/Program.cs @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////// +// +// FlashCap - Independent camera capture library. +// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) +// +// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 +// +//////////////////////////////////////////////////////////////////////////// + +using FlashCap.Devices; +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace FlashCap.Properties; + +public static class Program +{ + private static async Task SetCameraPropertyAsync(CancellationToken ct) + { + /////////////////////////////////////////////////////////////// + // Initialize and detection capture devices. + + // Step 1: Enumerate capture devices: + var devices = new CaptureDevices(); + var descriptor0 = devices.EnumerateDescriptors() + .Where(d => d.Characteristics.Length >= 1) + // Only DirectShow Properties are available + .Where(d => d.DeviceType == DeviceTypes.DirectShow) + .FirstOrDefault(); + if (descriptor0 == null) + { + Console.WriteLine($"Could not detect any capture interfaces."); + return 0; + } + + var characteristics0 = descriptor0.Characteristics. + FirstOrDefault(); + if (characteristics0 == null) + { + Console.WriteLine($"Could not select primary characteristics."); + return 0; + } + + Console.WriteLine($"Selected capture device: {descriptor0}, {characteristics0}"); + + /////////////////////////////////////////////////////////////// + var captureDevice = await descriptor0.OpenAsync( + characteristics0, + bufferScope => + { + var image = bufferScope.Buffer.CopyImage(); + + Console.WriteLine($"Captured {image.Length} bytes."); + + bufferScope.ReleaseNow(); + }, + ct); + + captureDevice.DisplayPropertyPage_CaptureFilter(IntPtr.Zero); + + foreach (var property in captureDevice.Properties) + { + Console.WriteLine($"Supported proprety {property.Key} - min value {property.Value.Min} - max value {property.Value.Max} - step {property.Value.Step}"); + } + + var brightnessProperty = captureDevice.Properties.Where(x => x.Key == VideoProcessingAmplifierProperty.Brightness).Select(x => x.Value).FirstOrDefault() ?? throw new Exception("Brightness is not supported with current device"); + + + captureDevice.SetPropertyValue(VideoProcessingAmplifierProperty.Brightness, brightnessProperty.Min); + Console.WriteLine($"Brightness property value updated with {brightnessProperty.Min}"); + + var brightness = captureDevice.GetPropertyValue(VideoProcessingAmplifierProperty.Brightness); + Console.WriteLine($"Brightness property value is {brightness}"); + + return 0; + } + + public static async Task Main(string[] args) + { + try + { + return await SetCameraPropertyAsync(default); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return Marshal.GetHRForException(ex); + } + } +} From e3d8f0b0bef5601b55249693e139d1bb633a656d Mon Sep 17 00:00:00 2001 From: ldlac Date: Wed, 5 Apr 2023 17:34:59 -0400 Subject: [PATCH 04/13] add DisplayPropertyPage_CaptureFilter --- FlashCap.Core/Devices/DirectShowDevice.cs | 53 +++++++++++++++++++ .../Internal/NativeMethods_DirectShow.cs | 48 +++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 9bb8b8d..3cf17aa 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -312,6 +312,59 @@ protected override Task OnStopAsync(CancellationToken ct) } } + + public void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + if (graphBuilder != null) + { + graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); + DisplayPropertyPage(captureSourceFilter, hwndOwner); + } + } + + private void DisplayPropertyPage(object? filterOrPin, IntPtr hwndOwner) + { + if (filterOrPin == null) + return; + + if (filterOrPin is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) + return; + + string caption = string.Empty; + if (filterOrPin is NativeMethods_DirectShow.IBaseFilter) + { + if (filterOrPin is not NativeMethods_DirectShow.IBaseFilter filter) + return; + + filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo); + + caption = filterInfo.chName; + + if (filterInfo.graph != null) + { + Marshal.ReleaseComObject(filterInfo.graph); + } + } + else if (filterOrPin is NativeMethods_DirectShow.IPin) + { + if (filterOrPin is not NativeMethods_DirectShow.IPin pin) + return; + + pin.QueryPinInfo(out NativeMethods_DirectShow.PIN_INFO pinInfo); + + caption = pinInfo.name; + } + + + pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID); + + object oDevice = filterOrPin; + NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, caption, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero); + + Marshal.FreeCoTaskMem(caGUID.pElems); + Marshal.ReleaseComObject(pProp); + } + #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 3c40c48..081ce5e 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -297,6 +297,54 @@ public struct FILTER_INFO public IFilterGraph graph; } + + [StructLayout(LayoutKind.Sequential)] + public struct DsCAUUID + { + public int cElems; + public IntPtr pElems; + + public Guid[] ToGuidArray() + { + Guid[] retval = new Guid[cElems]; + + for (int i = 0; i < cElems; i++) + { +#pragma warning disable CS0618 + IntPtr ptr = new(pElems.ToInt64() + (Marshal.SizeOf(typeof(Guid)) * i)); + Guid? guid = (Guid?)Marshal.PtrToStructure(ptr, typeof(Guid)); + retval[i] = guid.GetValueOrDefault(); +#pragma warning restore CS0618 + } + + return retval; + } + } + + [DllImport("oleaut32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)] + public static extern int OleCreatePropertyFrame( + IntPtr hwndOwner, + int x, + int y, + [MarshalAs(UnmanagedType.LPWStr)] string lpszCaption, + int cObjects, + [MarshalAs(UnmanagedType.Interface, ArraySubType = UnmanagedType.IUnknown)] + ref object ppUnk, + int cPages, + IntPtr lpPageClsID, + int lcid, + int dwReserved, + IntPtr lpvReserved); + + [SuppressUnmanagedCodeSecurity, + Guid("B196B28B-BAB4-101A-B69C-00AA00341D07"), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISpecifyPropertyPages + { + [PreserveSig] + int GetPages(out DsCAUUID pPages); + } + [SuppressUnmanagedCodeSecurity] [Guid("56a86895-0ad4-11ce-b03a-0020af0ba770")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] From 49bc98c3807769984dbde51afd26d0ee0e32d46d Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 10:23:06 -0400 Subject: [PATCH 05/13] . --- FlashCap.Core/Devices/DirectShowDevice.cs | 53 ++++++++++------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 3cf17aa..8448314 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -318,51 +318,46 @@ public void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) if (graphBuilder != null) { graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); - DisplayPropertyPage(captureSourceFilter, hwndOwner); + DisplayPropertyPage_Filter(captureSourceFilter, hwndOwner); } } - private void DisplayPropertyPage(object? filterOrPin, IntPtr hwndOwner) + private void DisplayPropertyPage_Filter(object? obj, IntPtr hwndOwner) { - if (filterOrPin == null) + if (obj == null) return; - if (filterOrPin is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) + if (obj is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) return; - string caption = string.Empty; - if (filterOrPin is NativeMethods_DirectShow.IBaseFilter) - { - if (filterOrPin is not NativeMethods_DirectShow.IBaseFilter filter) - return; - - filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo); - - caption = filterInfo.chName; + if (obj is not NativeMethods_DirectShow.IBaseFilter filter) + return; - if (filterInfo.graph != null) - { - Marshal.ReleaseComObject(filterInfo.graph); - } - } - else if (filterOrPin is NativeMethods_DirectShow.IPin) + if (filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo) < 0) { - if (filterOrPin is not NativeMethods_DirectShow.IPin pin) - return; - - pin.QueryPinInfo(out NativeMethods_DirectShow.PIN_INFO pinInfo); - - caption = pinInfo.name; + throw new ArgumentException( + $"FlashCap: Couldn't query filter info"); } + if (pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID) < 0) + { + throw new ArgumentException( + $"FlashCap: Couldn't get pages"); + } - pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID); - - object oDevice = filterOrPin; - NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, caption, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero); + object oDevice = obj; + if (NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, filterInfo.chName, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero) < 0) + { + throw new ArgumentException( + $"FlashCap: Couldn't create property frame"); + } Marshal.FreeCoTaskMem(caGUID.pElems); Marshal.ReleaseComObject(pProp); + if (filterInfo.graph != null) + { + Marshal.ReleaseComObject(filterInfo.graph); + } } #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP From 84794d1cabe6a855ae5e9ba033b436e5b7775f23 Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 22:57:05 -0400 Subject: [PATCH 06/13] testing with brightness --- FlashCap.Core/CaptureDevice.cs | 5 + FlashCap.Core/CaptureDeviceProperties.cs | 45 +++++++++ FlashCap.Core/Devices/DirectShowDevice.cs | 65 +++++++++++-- FlashCap.Core/Devices/DirectShowProperty.cs | 14 +++ FlashCap.Core/Devices/V4L2Device.cs | 41 +++++--- .../Devices/VideoForWindowsDevice.cs | 27 ++++-- .../Internal/NativeMethods_DirectShow.cs | 60 ++++++++++++ FlashCap.sln | 9 +- .../FlashCap.Properties.csproj | 18 ++++ samples/FlashCap.Properties/Program.cs | 94 +++++++++++++++++++ 10 files changed, 352 insertions(+), 26 deletions(-) create mode 100644 FlashCap.Core/CaptureDeviceProperties.cs create mode 100644 FlashCap.Core/Devices/DirectShowProperty.cs create mode 100644 samples/FlashCap.Properties/FlashCap.Properties.csproj create mode 100644 samples/FlashCap.Properties/Program.cs diff --git a/FlashCap.Core/CaptureDevice.cs b/FlashCap.Core/CaptureDevice.cs index f1bf3dd..cac48c3 100644 --- a/FlashCap.Core/CaptureDevice.cs +++ b/FlashCap.Core/CaptureDevice.cs @@ -66,6 +66,11 @@ protected abstract Task OnInitializeAsync( protected abstract Task OnStartAsync(CancellationToken ct); protected abstract Task OnStopAsync(CancellationToken ct); + public abstract void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner); + public abstract int GetPropertyValue(VideoProcessingAmplifierProperty property); + public abstract void SetPropertyValue(VideoProcessingAmplifierProperty property, object? value); + + public CaptureDeviceProperties Properties { get; protected set; } = new CaptureDeviceProperties(); protected abstract void OnCapture( IntPtr pData, int size, long timestampMicroseconds, long frameIndex, PixelBuffer buffer); diff --git a/FlashCap.Core/CaptureDeviceProperties.cs b/FlashCap.Core/CaptureDeviceProperties.cs new file mode 100644 index 0000000..85ead90 --- /dev/null +++ b/FlashCap.Core/CaptureDeviceProperties.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace FlashCap +{ + public enum VideoProcessingAmplifierProperty + { + Brightness, + Contrast, + Hue, + Saturation, + Sharpness, + Gamma, + ColorEnable, + WhiteBalance, + BacklightCompensation, + Gain + } + + public sealed class CaptureDeviceProperties : Dictionary + { + } + + public abstract class CaptureDeviceProperty + { + public VideoProcessingAmplifierProperty Property { get; private set; } + public int Min { get; private set; } + public int Max { get; private set; } + public int Step { get; private set; } + + protected CaptureDeviceProperty(VideoProcessingAmplifierProperty property, int min, int max, int step) + { + Property = property; + Min = min; + Max = max; + Step = step; + } + + internal bool IsPropertyValueValid(object? obj) + { + var value = obj as int?; + + return value != null && value <= Max && value >= Min && value % Step == 0; + } + } +} diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 8448314..1faaaaa 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -73,6 +73,7 @@ [PreserveSig] public int BufferCB( private bool transcodeIfYUV; private FrameProcessor frameProcessor; private NativeMethods_DirectShow.IGraphBuilder? graphBuilder; + private NativeMethods_DirectShow.ICaptureGraphBuilder2? captureGraphBuilder; private SampleGrabberSink? sampleGrabberSink; private IntPtr pBih; @@ -174,7 +175,7 @@ protected override Task OnInitializeAsync( /////////////////////////////// - var captureGraphBuilder = NativeMethods_DirectShow.CreateCaptureGraphBuilder(); + captureGraphBuilder = NativeMethods_DirectShow.CreateCaptureGraphBuilder(); if (captureGraphBuilder.SetFiltergraph(this.graphBuilder) < 0) { throw new ArgumentException( @@ -312,12 +313,64 @@ protected override Task OnStopAsync(CancellationToken ct) } } + public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + { + if (Properties.TryGetValue(property, out CaptureDeviceProperty _) + && graphBuilder != null + && captureGraphBuilder != null) + { + graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); + if (captureSourceFilter != null) + { + captureGraphBuilder.FindInterface(Guid.Empty, Guid.Empty, captureSourceFilter, NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, out object? videoProcAmpObject); + if (videoProcAmpObject != null) + { + var videoProcAmp = (NativeMethods_DirectShow.IAMVideoProcAmp)videoProcAmpObject; + videoProcAmp.Get(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), out int value, out NativeMethods_DirectShow.VideoProcAmpFlags _); + Marshal.ReleaseComObject(videoProcAmpObject); + return value; + } + Marshal.ReleaseComObject(captureGraphBuilder); + } + } + + throw new ArgumentException( + $"FlashCap: Property is not supported by device: Property={property}"); + } - public void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) { - if (graphBuilder != null) + if (Properties.TryGetValue(property, out CaptureDeviceProperty? captureDeviceProperty) + && obj != null + && captureDeviceProperty != null + && captureDeviceProperty.IsPropertyValueValid(obj) + && graphBuilder != null + && captureGraphBuilder != null) { graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); + if (captureSourceFilter != null) + { + captureGraphBuilder.FindInterface(Guid.Empty, Guid.Empty, captureSourceFilter, NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, out object? videoProcAmpObject); + if (videoProcAmpObject != null) + { + var videoProcAmp = (NativeMethods_DirectShow.IAMVideoProcAmp)videoProcAmpObject; + videoProcAmp.Get(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), out int _, out NativeMethods_DirectShow.VideoProcAmpFlags videoProcAmpFlags); + videoProcAmp.Set(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), (int)obj, videoProcAmpFlags); + Marshal.ReleaseComObject(videoProcAmpObject); + } + Marshal.ReleaseComObject(captureGraphBuilder); + } + } + + throw new ArgumentException( + $"FlashCap: Property is not supported by device: Property={property}"); + } + + public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + if (graphBuilder != null + && graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter) >= 0) + { DisplayPropertyPage_Filter(captureSourceFilter, hwndOwner); } } @@ -335,20 +388,20 @@ private void DisplayPropertyPage_Filter(object? obj, IntPtr hwndOwner) if (filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo) < 0) { - throw new ArgumentException( + throw new Exception( $"FlashCap: Couldn't query filter info"); } if (pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID) < 0) { - throw new ArgumentException( + throw new Exception( $"FlashCap: Couldn't get pages"); } object oDevice = obj; if (NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, filterInfo.chName, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero) < 0) { - throw new ArgumentException( + throw new Exception( $"FlashCap: Couldn't create property frame"); } diff --git a/FlashCap.Core/Devices/DirectShowProperty.cs b/FlashCap.Core/Devices/DirectShowProperty.cs new file mode 100644 index 0000000..5b78099 --- /dev/null +++ b/FlashCap.Core/Devices/DirectShowProperty.cs @@ -0,0 +1,14 @@ +using FlashCap.Internal; + +namespace FlashCap.Devices +{ + public sealed class DirectShowProperty : CaptureDeviceProperty + { + public DirectShowProperty(VideoProcessingAmplifierProperty property, int min, int max, int step) : base(property, min, max, step) { } + + internal static NativeMethods_DirectShow.VideoProcAmpProperty FromVideoProcessingAmplifierProperty(VideoProcessingAmplifierProperty property) + { + return (NativeMethods_DirectShow.VideoProcAmpProperty)(int)property; + } + } +} diff --git a/FlashCap.Core/Devices/V4L2Device.cs b/FlashCap.Core/Devices/V4L2Device.cs index 366ec2a..bfd98af 100644 --- a/FlashCap.Core/Devices/V4L2Device.cs +++ b/FlashCap.Core/Devices/V4L2Device.cs @@ -31,10 +31,10 @@ public sealed class V4L2Device : CaptureDevice private FrameProcessor frameProcessor; private long frameIndex; - + private readonly IntPtr[] pBuffers = new IntPtr[BufferCount]; private readonly int[] bufferLength = new int[BufferCount]; - + private int fd; private IntPtr pBih; private Task task; @@ -93,11 +93,11 @@ protected override unsafe Task OnInitializeAsync( fmt_pix.height = (uint)characteristics.Height; fmt_pix.pixelformat = pix_fmt; fmt_pix.field = (uint)v4l2_field.ANY; - + var format = Interop.Create_v4l2_format(); format.type = (uint)v4l2_buf_type.VIDEO_CAPTURE; format.fmt_pix = fmt_pix; - + if (ioctl(fd, Interop.VIDIOC_S_FMT, format) == 0) { applied = true; @@ -128,14 +128,14 @@ protected override unsafe Task OnInitializeAsync( buffer.type = (uint)v4l2_buf_type.VIDEO_CAPTURE; buffer.memory = (uint)v4l2_memory.MMAP; buffer.index = (uint)index; - + if (ioctl(fd, Interop.VIDIOC_QUERYBUF, buffer) < 0) { var code = Marshal.GetLastWin32Error(); throw new ArgumentException( $"FlashCap: Couldn't assign video buffer: Code={code}, DevicePath={this.devicePath}"); } - + if (mmap(IntPtr.Zero, buffer.length, PROT.READ, MAP.SHARED, fd, buffer.m_offset) is { } pBuffer && pBuffer == MAP_FAILED) @@ -178,7 +178,7 @@ protected override unsafe Task OnInitializeAsync( pBih->biWidth = characteristics.Width; pBih->biHeight = characteristics.Height; pBih->biSizeImage = pBih->CalculateImageSize(); - + this.fd = fd; this.pBih = pih; @@ -203,7 +203,7 @@ protected override unsafe Task OnInitializeAsync( this.bufferLength[index] = default; } } - + close(fd); throw; } @@ -234,7 +234,7 @@ await this.frameProcessor.DisposeAsync(). this.fd, Interop.VIDIOC_STREAMOFF, (int)v4l2_buf_type.VIDEO_CAPTURE); } - + write(this.abortwfd, new byte[] { 0x01 }, 1); await this.task. @@ -254,7 +254,7 @@ static bool IsIgnore(int code) => EINVAL => true, _ => false, }; - + var fds = new[] { new pollfd @@ -361,12 +361,12 @@ static bool IsIgnore(int code) => this.bufferLength[index] = default; } } - + close(this.abortrfd); close(this.fd); - + NativeMethods.FreeMemory(this.pBih); - + this.abortrfd = -1; this.fd = -1; this.pBih = IntPtr.Zero; @@ -419,6 +419,21 @@ protected override Task OnStopAsync(CancellationToken ct) return TaskCompat.CompletedTask; } + public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + throw new Exception("not supported for V4l2Device"); + } + #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif diff --git a/FlashCap.Core/Devices/VideoForWindowsDevice.cs b/FlashCap.Core/Devices/VideoForWindowsDevice.cs index 2407142..1b7f541 100644 --- a/FlashCap.Core/Devices/VideoForWindowsDevice.cs +++ b/FlashCap.Core/Devices/VideoForWindowsDevice.cs @@ -135,15 +135,15 @@ protected override unsafe Task OnInitializeAsync( throw; } - /////////////////////////////////////// + /////////////////////////////////////// - this.handle = handle; + this.handle = handle; - // https://stackoverflow.com/questions/4097235/is-it-necessary-to-gchandle-alloc-each-callback-in-a-class - this.thisPin = GCHandle.Alloc(this, GCHandleType.Normal); - this.callback = this.CallbackEntry; + // https://stackoverflow.com/questions/4097235/is-it-necessary-to-gchandle-alloc-each-callback-in-a-class + this.thisPin = GCHandle.Alloc(this, GCHandleType.Normal); + this.callback = this.CallbackEntry; - NativeMethods_VideoForWindows.capSetCallbackFrame(handle, this.callback); + NativeMethods_VideoForWindows.capSetCallbackFrame(handle, this.callback); }, ct); } @@ -279,6 +279,21 @@ protected override Task OnStopAsync(CancellationToken ct) } } + public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) + { + throw new Exception("not supported for V4l2Device"); + } + + public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + { + throw new Exception("not supported for V4l2Device"); + } + #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 081ce5e..66fa254 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -622,6 +622,66 @@ [PreserveSig] int get_RegFilterCollection( //////////////////////////////////////////////////////////////////////// + public static class IAMVideoProcAmpHelper + { + public static Guid GUID = new("C6E13360-30AC-11d0-A18C-00A0C9118956"); + } + + [ComImport, System.Security.SuppressUnmanagedCodeSecurity] + [Guid("C6E13360-30AC-11d0-A18C-00A0C9118956")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IAMVideoProcAmp + { + [PreserveSig] + int GetRange( + [In] VideoProcAmpProperty Property, + [Out] out int pMin, + [Out] out int pMax, + [Out] out int pSteppingDelta, + [Out] out int pDefault, + [Out] out VideoProcAmpFlags pCapsFlags + ); + + [PreserveSig] + int Set( + [In] VideoProcAmpProperty Property, + [In] int lValue, + [In] VideoProcAmpFlags Flags + ); + + [PreserveSig] + int Get( + [In] VideoProcAmpProperty Property, + [Out] out int lValue, + [Out] out VideoProcAmpFlags Flags + ); + } + + + [Flags] + public enum VideoProcAmpFlags + { + None = 0, + Auto = 0x0001, + Manual = 0x0002 + } + + public enum VideoProcAmpProperty + { + Brightness, + Contrast, + Hue, + Saturation, + Sharpness, + Gamma, + ColorEnable, + WhiteBalance, + BacklightCompensation, + Gain + } + + //////////////////////////////////////////////////////////////////////// + public static readonly Guid CLSID_SystemDeviceEnum = new Guid("62BE5D10-60EB-11d0-BD3B-00A0C911CE86"); public static readonly Guid CLSID_VideoInputDeviceCategory = diff --git a/FlashCap.sln b/FlashCap.sln index 5233699..dffd003 100644 --- a/FlashCap.sln +++ b/FlashCap.sln @@ -35,7 +35,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.Core", "FlashCap.C EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.FlashCap", "FSharp.FlashCap\FSharp.FlashCap.fsproj", "{AAD7F2E9-C6F7-4BDA-9D49-08469C2C7A1D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlashCap.OneShot", "samples\FlashCap.OneShot\FlashCap.OneShot.csproj", "{2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.OneShot", "samples\FlashCap.OneShot\FlashCap.OneShot.csproj", "{2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.Properties", "samples\FlashCap.Properties\FlashCap.Properties.csproj", "{273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -79,6 +81,10 @@ Global {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}.Release|Any CPU.Build.0 = Release|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -90,6 +96,7 @@ Global {B8B4CFA8-4360-4503-A316-03EAD02AD96E} = {A1BDFB2A-48C0-4D65-AE03-4DBFC12B4BA8} {583903DC-9D94-4A63-AFCB-2FB88CE14C95} = {655486CC-57A5-494E-97D0-9850B6D55BBA} {2914E44E-F6F4-4BF0-B0BB-962DE4D505AE} = {655486CC-57A5-494E-97D0-9850B6D55BBA} + {273C64E6-BFDA-4ECE-9FAE-37A50FFFE08D} = {655486CC-57A5-494E-97D0-9850B6D55BBA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {53E3C2D2-05FD-4396-ABF1-6B313F8F82A9} diff --git a/samples/FlashCap.Properties/FlashCap.Properties.csproj b/samples/FlashCap.Properties/FlashCap.Properties.csproj new file mode 100644 index 0000000..e6627bd --- /dev/null +++ b/samples/FlashCap.Properties/FlashCap.Properties.csproj @@ -0,0 +1,18 @@ + + + + Exe + net48;net6.0 + latest + Enable + + + + + + + + + + + diff --git a/samples/FlashCap.Properties/Program.cs b/samples/FlashCap.Properties/Program.cs new file mode 100644 index 0000000..57d3638 --- /dev/null +++ b/samples/FlashCap.Properties/Program.cs @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////// +// +// FlashCap - Independent camera capture library. +// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud) +// +// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0 +// +//////////////////////////////////////////////////////////////////////////// + +using FlashCap.Devices; +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace FlashCap.Properties; + +public static class Program +{ + private static async Task SetCameraPropertyAsync(CancellationToken ct) + { + /////////////////////////////////////////////////////////////// + // Initialize and detection capture devices. + + // Step 1: Enumerate capture devices: + var devices = new CaptureDevices(); + var descriptor0 = devices.EnumerateDescriptors() + .Where(d => d.Characteristics.Length >= 1) + // Only DirectShow Properties are available + .Where(d => d.DeviceType == DeviceTypes.DirectShow) + .FirstOrDefault(); + if (descriptor0 == null) + { + Console.WriteLine($"Could not detect any capture interfaces."); + return 0; + } + + var characteristics0 = descriptor0.Characteristics. + FirstOrDefault(); + if (characteristics0 == null) + { + Console.WriteLine($"Could not select primary characteristics."); + return 0; + } + + Console.WriteLine($"Selected capture device: {descriptor0}, {characteristics0}"); + + /////////////////////////////////////////////////////////////// + var captureDevice = await descriptor0.OpenAsync( + characteristics0, + bufferScope => + { + var image = bufferScope.Buffer.CopyImage(); + + Console.WriteLine($"Captured {image.Length} bytes."); + + bufferScope.ReleaseNow(); + }, + ct); + + captureDevice.DisplayPropertyPage_CaptureFilter(IntPtr.Zero); + + foreach (var property in captureDevice.Properties) + { + Console.WriteLine($"Supported proprety {property.Key} - min value {property.Value.Min} - max value {property.Value.Max} - step {property.Value.Step}"); + } + + var brightnessProperty = captureDevice.Properties.Where(x => x.Key == VideoProcessingAmplifierProperty.Brightness).Select(x => x.Value).FirstOrDefault() ?? throw new Exception("Brightness is not supported with current device"); + + + captureDevice.SetPropertyValue(VideoProcessingAmplifierProperty.Brightness, brightnessProperty.Min); + Console.WriteLine($"Brightness property value updated with {brightnessProperty.Min}"); + + var brightness = captureDevice.GetPropertyValue(VideoProcessingAmplifierProperty.Brightness); + Console.WriteLine($"Brightness property value is {brightness}"); + + return 0; + } + + public static async Task Main(string[] args) + { + try + { + return await SetCameraPropertyAsync(default); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return Marshal.GetHRForException(ex); + } + } +} From ffd33412d91ada47683e2bd2007f4855b3addfc3 Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 23:04:16 -0400 Subject: [PATCH 07/13] fill dict --- FlashCap.Core/Devices/DirectShowDevice.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 1faaaaa..91a7a71 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -182,6 +182,17 @@ protected override Task OnInitializeAsync( $"FlashCap: Couldn't set graph builder: DevicePath={devicePath}"); } + /////////////////////////////// + + captureGraphBuilder.FindInterface(Guid.Empty, Guid.Empty, captureSource, NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, out object? videoProcAmpObject); + if (videoProcAmpObject != null) + { + var videoProcAmp = (NativeMethods_DirectShow.IAMVideoProcAmp)videoProcAmpObject; + videoProcAmp.GetRange(NativeMethods_DirectShow.VideoProcAmpProperty.Brightness, out int brightnessMin, out int brightnessMax, out int steppingDelta, out int def, out NativeMethods_DirectShow.VideoProcAmpFlags videoProcAmpFlags); + Properties.Add(VideoProcessingAmplifierProperty.Brightness, new DirectShowProperty(VideoProcessingAmplifierProperty.Brightness, brightnessMin, brightnessMax, steppingDelta)); + Marshal.ReleaseComObject(videoProcAmpObject); + } + /////////////////////////////// if (captureGraphBuilder.RenderStream( From effa8cd574ddb14eed476b9f9a3c4c8180c653d5 Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 23:15:05 -0400 Subject: [PATCH 08/13] . --- FlashCap.Core/Devices/VideoForWindowsDevice.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FlashCap.Core/Devices/VideoForWindowsDevice.cs b/FlashCap.Core/Devices/VideoForWindowsDevice.cs index 1b7f541..63c6d37 100644 --- a/FlashCap.Core/Devices/VideoForWindowsDevice.cs +++ b/FlashCap.Core/Devices/VideoForWindowsDevice.cs @@ -281,17 +281,17 @@ protected override Task OnStopAsync(CancellationToken ct) public override int GetPropertyValue(VideoProcessingAmplifierProperty property) { - throw new Exception("not supported for V4l2Device"); + throw new Exception("not supported for VideoForWindows"); } public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) { - throw new Exception("not supported for V4l2Device"); + throw new Exception("not supported for VideoForWindows"); } public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) { - throw new Exception("not supported for V4l2Device"); + throw new Exception("not supported for VideoForWindows"); } #if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP From 4bd01b35986a382e021cebbd9901786f0fd1aea3 Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 23:16:15 -0400 Subject: [PATCH 09/13] . --- samples/FlashCap.Properties/Program.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/FlashCap.Properties/Program.cs b/samples/FlashCap.Properties/Program.cs index 57d3638..0a32f44 100644 --- a/samples/FlashCap.Properties/Program.cs +++ b/samples/FlashCap.Properties/Program.cs @@ -60,8 +60,6 @@ private static async Task SetCameraPropertyAsync(CancellationToken ct) }, ct); - captureDevice.DisplayPropertyPage_CaptureFilter(IntPtr.Zero); - foreach (var property in captureDevice.Properties) { Console.WriteLine($"Supported proprety {property.Key} - min value {property.Value.Min} - max value {property.Value.Max} - step {property.Value.Step}"); From 85c88308446c558b09e2ef9600016a370ce4a497 Mon Sep 17 00:00:00 2001 From: ldlac Date: Thu, 6 Apr 2023 23:18:51 -0400 Subject: [PATCH 10/13] . --- FlashCap.Core/Devices/DirectShowDevice.cs | 2 -- samples/FlashCap.Properties/Program.cs | 1 - 2 files changed, 3 deletions(-) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 91a7a71..abadae5 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -341,7 +341,6 @@ public override int GetPropertyValue(VideoProcessingAmplifierProperty property) Marshal.ReleaseComObject(videoProcAmpObject); return value; } - Marshal.ReleaseComObject(captureGraphBuilder); } } @@ -369,7 +368,6 @@ public override void SetPropertyValue(VideoProcessingAmplifierProperty property, videoProcAmp.Set(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), (int)obj, videoProcAmpFlags); Marshal.ReleaseComObject(videoProcAmpObject); } - Marshal.ReleaseComObject(captureGraphBuilder); } } diff --git a/samples/FlashCap.Properties/Program.cs b/samples/FlashCap.Properties/Program.cs index 0a32f44..7cfd852 100644 --- a/samples/FlashCap.Properties/Program.cs +++ b/samples/FlashCap.Properties/Program.cs @@ -67,7 +67,6 @@ private static async Task SetCameraPropertyAsync(CancellationToken ct) var brightnessProperty = captureDevice.Properties.Where(x => x.Key == VideoProcessingAmplifierProperty.Brightness).Select(x => x.Value).FirstOrDefault() ?? throw new Exception("Brightness is not supported with current device"); - captureDevice.SetPropertyValue(VideoProcessingAmplifierProperty.Brightness, brightnessProperty.Min); Console.WriteLine($"Brightness property value updated with {brightnessProperty.Min}"); From 0c4fa112ee870e4f5c0fe4a85fcec831a5d9bc2c Mon Sep 17 00:00:00 2001 From: ldlac Date: Sat, 22 Apr 2023 12:04:15 -0400 Subject: [PATCH 11/13] . --- .../Internal/NativeMethods_DirectShow.cs | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 66fa254..53a03b6 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -263,7 +263,7 @@ public interface IEnumFilters { [PreserveSig] int Next( int request, - [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] IPin?[] filters, + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] IBaseFilter?[] filters, out int fetched); [PreserveSig] int Skip(int count); [PreserveSig] int Reset(); @@ -622,6 +622,45 @@ [PreserveSig] int get_RegFilterCollection( //////////////////////////////////////////////////////////////////////// + public static IBaseFilter? FindFilterByName(IGraphBuilder graphBuilder, string filterName) + { + int hr = 0; + IBaseFilter? filter = null; + IEnumFilters? enumFilters = null; + + if (graphBuilder == null) + throw new ArgumentNullException("graphBuilder"); + + hr = graphBuilder.EnumFilters(out enumFilters); + if (hr == 0 && enumFilters != null) + { + IBaseFilter[] filters = new IBaseFilter[1]; + + while (enumFilters.Next(filters.Length, filters, out int val) == 0) + { + FILTER_INFO filterInfo; + + hr = filters[0].QueryFilterInfo(out filterInfo); + if (hr == 0) + { + if (filterInfo.graph != null) + Marshal.ReleaseComObject(filterInfo.graph); + + if (filterInfo.chName.Equals(filterName)) + { + filter = filters[0]; + break; + } + } + + Marshal.ReleaseComObject(filters[0]); + } + Marshal.ReleaseComObject(enumFilters); + } + + return filter; + } + public static class IAMVideoProcAmpHelper { public static Guid GUID = new("C6E13360-30AC-11d0-A18C-00A0C9118956"); From 2fdc18b106df17c14fe4fa3d1a25697d7ed65068 Mon Sep 17 00:00:00 2001 From: Kouji Matsui Date: Wed, 19 Apr 2023 21:17:34 +0900 Subject: [PATCH 12/13] Fixed invalid cross apartment access at DS pages and properties. --- FlashCap.Core/CaptureDevice.cs | 10 +- FlashCap.Core/Devices/DirectShowDevice.cs | 149 ++++++++++-------- FlashCap.Core/Devices/V4L2Device.cs | 9 +- .../Devices/VideoForWindowsDevice.cs | 9 +- .../IndependentSingleApartmentContext.cs | 21 +++ .../Internal/NativeMethods_DirectShow.cs | 4 +- samples/FlashCap.Properties/Program.cs | 10 +- 7 files changed, 136 insertions(+), 76 deletions(-) diff --git a/FlashCap.Core/CaptureDevice.cs b/FlashCap.Core/CaptureDevice.cs index cac48c3..e71c892 100644 --- a/FlashCap.Core/CaptureDevice.cs +++ b/FlashCap.Core/CaptureDevice.cs @@ -66,9 +66,13 @@ protected abstract Task OnInitializeAsync( protected abstract Task OnStartAsync(CancellationToken ct); protected abstract Task OnStopAsync(CancellationToken ct); - public abstract void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner); - public abstract int GetPropertyValue(VideoProcessingAmplifierProperty property); - public abstract void SetPropertyValue(VideoProcessingAmplifierProperty property, object? value); + + public abstract Task ShowPropertyPageAsync( + IntPtr hwndOwner, CancellationToken ct = default); + public abstract Task GetPropertyValueAsync( + VideoProcessingAmplifierProperty property, CancellationToken ct = default); + public abstract Task SetPropertyValueAsync( + VideoProcessingAmplifierProperty property, object? value, CancellationToken ct = default); public CaptureDeviceProperties Properties { get; protected set; } = new CaptureDeviceProperties(); diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index abadae5..d02aad9 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -74,6 +74,7 @@ [PreserveSig] public int BufferCB( private FrameProcessor frameProcessor; private NativeMethods_DirectShow.IGraphBuilder? graphBuilder; private NativeMethods_DirectShow.ICaptureGraphBuilder2? captureGraphBuilder; + private NativeMethods_DirectShow.IBaseFilter? captureSource; private SampleGrabberSink? sampleGrabberSink; private IntPtr pBih; @@ -175,7 +176,7 @@ protected override Task OnInitializeAsync( /////////////////////////////// - captureGraphBuilder = NativeMethods_DirectShow.CreateCaptureGraphBuilder(); + this.captureGraphBuilder = NativeMethods_DirectShow.CreateCaptureGraphBuilder(); if (captureGraphBuilder.SetFiltergraph(this.graphBuilder) < 0) { throw new ArgumentException( @@ -195,7 +196,7 @@ protected override Task OnInitializeAsync( /////////////////////////////// - if (captureGraphBuilder.RenderStream( + if (this.captureGraphBuilder.RenderStream( in NativeMethods_DirectShow.PIN_CATEGORY_CAPTURE, in NativeMethods_DirectShow.MEDIATYPE_Video, captureSource, @@ -225,6 +226,8 @@ protected override Task OnInitializeAsync( throw new ArgumentException( $"FlashCap: Couldn't get grabbing media type: DevicePath={devicePath}"); } + + this.captureSource = captureSource; } catch { @@ -324,16 +327,18 @@ protected override Task OnStopAsync(CancellationToken ct) } } - public override int GetPropertyValue(VideoProcessingAmplifierProperty property) - { - if (Properties.TryGetValue(property, out CaptureDeviceProperty _) - && graphBuilder != null - && captureGraphBuilder != null) + public override Task GetPropertyValueAsync( + VideoProcessingAmplifierProperty property, CancellationToken ct = default) => + this.workingContext!.InvokeAsync(() => { - graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); - if (captureSourceFilter != null) + if (this.Properties.TryGetValue(property, out var _) && + this.captureGraphBuilder != null && + this.captureSource != null) { - captureGraphBuilder.FindInterface(Guid.Empty, Guid.Empty, captureSourceFilter, NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, out object? videoProcAmpObject); + this.captureGraphBuilder.FindInterface( + Guid.Empty, Guid.Empty, this.captureSource, + NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, + out object? videoProcAmpObject); if (videoProcAmpObject != null) { var videoProcAmp = (NativeMethods_DirectShow.IAMVideoProcAmp)videoProcAmpObject; @@ -342,25 +347,26 @@ public override int GetPropertyValue(VideoProcessingAmplifierProperty property) return value; } } - } - throw new ArgumentException( - $"FlashCap: Property is not supported by device: Property={property}"); - } + throw new ArgumentException( + $"FlashCap: Property is not supported by device: Property={property}"); + }, ct); - public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) - { - if (Properties.TryGetValue(property, out CaptureDeviceProperty? captureDeviceProperty) - && obj != null - && captureDeviceProperty != null - && captureDeviceProperty.IsPropertyValueValid(obj) - && graphBuilder != null - && captureGraphBuilder != null) + public override Task SetPropertyValueAsync( + VideoProcessingAmplifierProperty property, object? obj, CancellationToken ct = default) => + this.workingContext!.InvokeAsync(() => { - graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter); - if (captureSourceFilter != null) + if (this.Properties.TryGetValue(property, out var captureDeviceProperty) && + obj != null && + captureDeviceProperty != null && + captureDeviceProperty.IsPropertyValueValid(obj) && + captureGraphBuilder != null && + this.captureSource != null) { - captureGraphBuilder.FindInterface(Guid.Empty, Guid.Empty, captureSourceFilter, NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, out object? videoProcAmpObject); + this.captureGraphBuilder.FindInterface( + Guid.Empty, Guid.Empty, this.captureSource, + NativeMethods_DirectShow.IAMVideoProcAmpHelper.GUID, + out object? videoProcAmpObject); if (videoProcAmpObject != null) { var videoProcAmp = (NativeMethods_DirectShow.IAMVideoProcAmp)videoProcAmpObject; @@ -369,56 +375,73 @@ public override void SetPropertyValue(VideoProcessingAmplifierProperty property, Marshal.ReleaseComObject(videoProcAmpObject); } } - } - throw new ArgumentException( - $"FlashCap: Property is not supported by device: Property={property}"); - } + throw new ArgumentException( + $"FlashCap: Property is not supported by device: Property={property}"); + }, ct); - public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) - { - if (graphBuilder != null - && graphBuilder.FindFilterByName("Capture source", out NativeMethods_DirectShow.IBaseFilter? captureSourceFilter) >= 0) + public override Task ShowPropertyPageAsync( + IntPtr hwndOwner, CancellationToken ct = default) => + this.workingContext!.InvokeAsync(() => { - DisplayPropertyPage_Filter(captureSourceFilter, hwndOwner); - } - } + if (this.captureSource != null) + { + InternalShowPropertyPage(hwndOwner, ct); + } + }, ct); - private void DisplayPropertyPage_Filter(object? obj, IntPtr hwndOwner) + private void InternalShowPropertyPage( + IntPtr hwndOwner, CancellationToken ct) { - if (obj == null) - return; - - if (obj is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) - return; - - if (obj is not NativeMethods_DirectShow.IBaseFilter filter) + if (this.captureSource is not NativeMethods_DirectShow.ISpecifyPropertyPages pProp) return; - if (filter.QueryFilterInfo(out NativeMethods_DirectShow.FILTER_INFO filterInfo) < 0) + try { - throw new Exception( - $"FlashCap: Couldn't query filter info"); - } + if (this.captureSource.QueryFilterInfo( + out NativeMethods_DirectShow.FILTER_INFO filterInfo) < 0) + { + throw new Exception( + $"FlashCap: Couldn't query filter info"); + } - if (pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID) < 0) - { - throw new Exception( - $"FlashCap: Couldn't get pages"); - } + try + { + if (pProp.GetPages(out NativeMethods_DirectShow.DsCAUUID caGUID) < 0) + { + throw new Exception( + $"FlashCap: Couldn't get pages"); + } - object oDevice = obj; - if (NativeMethods_DirectShow.OleCreatePropertyFrame(hwndOwner, 0, 0, filterInfo.chName, 1, ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero) < 0) - { - throw new Exception( - $"FlashCap: Couldn't create property frame"); - } + try + { + // TODO: Hook and will be aborted by CancellationToken. - Marshal.FreeCoTaskMem(caGUID.pElems); - Marshal.ReleaseComObject(pProp); - if (filterInfo.graph != null) + object oDevice = this.captureSource; + if (NativeMethods_DirectShow.OleCreatePropertyFrame( + hwndOwner, 0, 0, filterInfo.chName, 1, + ref oDevice, caGUID.cElems, caGUID.pElems, 0, 0, IntPtr.Zero) < 0) + { + throw new Exception( + $"FlashCap: Couldn't create property frame"); + } + } + finally + { + Marshal.FreeCoTaskMem(caGUID.pElems); + } + } + finally + { + if (filterInfo.graph != null) + { + Marshal.ReleaseComObject(filterInfo.graph); + } + } + } + finally { - Marshal.ReleaseComObject(filterInfo.graph); + Marshal.ReleaseComObject(pProp); } } diff --git a/FlashCap.Core/Devices/V4L2Device.cs b/FlashCap.Core/Devices/V4L2Device.cs index bfd98af..4c384a6 100644 --- a/FlashCap.Core/Devices/V4L2Device.cs +++ b/FlashCap.Core/Devices/V4L2Device.cs @@ -419,17 +419,20 @@ protected override Task OnStopAsync(CancellationToken ct) return TaskCompat.CompletedTask; } - public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + public override Task GetPropertyValueAsync( + VideoProcessingAmplifierProperty property, CancellationToken ct = default) { throw new Exception("not supported for V4l2Device"); } - public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) + public override Task SetPropertyValueAsync( + VideoProcessingAmplifierProperty property, object? obj, CancellationToken ct = default) { throw new Exception("not supported for V4l2Device"); } - public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + public override Task ShowPropertyPageAsync( + IntPtr hwndOwner, CancellationToken ct = default) { throw new Exception("not supported for V4l2Device"); } diff --git a/FlashCap.Core/Devices/VideoForWindowsDevice.cs b/FlashCap.Core/Devices/VideoForWindowsDevice.cs index 63c6d37..6c5436c 100644 --- a/FlashCap.Core/Devices/VideoForWindowsDevice.cs +++ b/FlashCap.Core/Devices/VideoForWindowsDevice.cs @@ -279,17 +279,20 @@ protected override Task OnStopAsync(CancellationToken ct) } } - public override int GetPropertyValue(VideoProcessingAmplifierProperty property) + public override Task GetPropertyValueAsync( + VideoProcessingAmplifierProperty property, CancellationToken ct = default) { throw new Exception("not supported for VideoForWindows"); } - public override void SetPropertyValue(VideoProcessingAmplifierProperty property, object? obj) + public override Task SetPropertyValueAsync( + VideoProcessingAmplifierProperty property, object? obj, CancellationToken ct = default) { throw new Exception("not supported for VideoForWindows"); } - public override void DisplayPropertyPage_CaptureFilter(IntPtr hwndOwner) + public override Task ShowPropertyPageAsync( + IntPtr hwndOwner, CancellationToken ct = default) { throw new Exception("not supported for VideoForWindows"); } diff --git a/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs b/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs index fbd1ae7..e573ccb 100644 --- a/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs +++ b/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs @@ -200,6 +200,27 @@ await tcs.Task. ConfigureAwait(false); } + public async Task InvokeAsync(Func action, CancellationToken ct) + { + var tcs = new TaskCompletionSource(); + using var _ = ct.Register(() => tcs.TrySetCanceled()); + + this.Post(_ => + { + try + { + tcs.TrySetResult(action()); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }, null); + + return await tcs.Task. + ConfigureAwait(false); + } + private void ThreadEntry() { PeekMessage(out var _, IntPtr.Zero, 0, 0, PM_NOREMOVE); diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 53a03b6..668c6c9 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -282,7 +282,7 @@ [PreserveSig] int AddFilter( [PreserveSig] int EnumFilters(out IEnumFilters? ppEnum); [PreserveSig] int FindFilterByName( [MarshalAs(UnmanagedType.LPWStr)] string name, - out IBaseFilter? filter); + [MarshalAs(UnmanagedType.IUnknown)] out object? filter); [PreserveSig] int ConnectDirect( IPin pinOut, IPin pinIn, in AM_MEDIA_TYPE mt); [PreserveSig] int Reconnect(IPin pin); @@ -444,7 +444,7 @@ public interface IGraphBuilder : IFilterGraph [PreserveSig] new int EnumFilters(out IEnumFilters? ppEnum); [PreserveSig] new int FindFilterByName( [MarshalAs(UnmanagedType.LPWStr)] string name, - out IBaseFilter? filter); + [MarshalAs(UnmanagedType.IUnknown)] out object? filter); [PreserveSig] new int ConnectDirect( IPin pinOut, IPin pinIn, in AM_MEDIA_TYPE mt); [PreserveSig] new int Reconnect(IPin pin); diff --git a/samples/FlashCap.Properties/Program.cs b/samples/FlashCap.Properties/Program.cs index 7cfd852..da576fc 100644 --- a/samples/FlashCap.Properties/Program.cs +++ b/samples/FlashCap.Properties/Program.cs @@ -48,6 +48,7 @@ private static async Task SetCameraPropertyAsync(CancellationToken ct) Console.WriteLine($"Selected capture device: {descriptor0}, {characteristics0}"); /////////////////////////////////////////////////////////////// + var captureDevice = await descriptor0.OpenAsync( characteristics0, bufferScope => @@ -60,6 +61,8 @@ private static async Task SetCameraPropertyAsync(CancellationToken ct) }, ct); + await captureDevice.ShowPropertyPageAsync(IntPtr.Zero); + foreach (var property in captureDevice.Properties) { Console.WriteLine($"Supported proprety {property.Key} - min value {property.Value.Min} - max value {property.Value.Max} - step {property.Value.Step}"); @@ -67,10 +70,13 @@ private static async Task SetCameraPropertyAsync(CancellationToken ct) var brightnessProperty = captureDevice.Properties.Where(x => x.Key == VideoProcessingAmplifierProperty.Brightness).Select(x => x.Value).FirstOrDefault() ?? throw new Exception("Brightness is not supported with current device"); - captureDevice.SetPropertyValue(VideoProcessingAmplifierProperty.Brightness, brightnessProperty.Min); + await captureDevice.SetPropertyValueAsync( + VideoProcessingAmplifierProperty.Brightness, + brightnessProperty.Min); Console.WriteLine($"Brightness property value updated with {brightnessProperty.Min}"); - var brightness = captureDevice.GetPropertyValue(VideoProcessingAmplifierProperty.Brightness); + var brightness = await captureDevice.GetPropertyValueAsync( + VideoProcessingAmplifierProperty.Brightness); Console.WriteLine($"Brightness property value is {brightness}"); return 0; From 323758b858cf3c85e8b5226ef90a01eb45b4b3e6 Mon Sep 17 00:00:00 2001 From: ldlac Date: Sat, 22 Apr 2023 12:15:52 -0400 Subject: [PATCH 13/13] missing return; sample restore original property value --- FlashCap.Core/Devices/DirectShowDevice.cs | 3 ++- samples/FlashCap.Properties/Program.cs | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index d02aad9..ca14a64 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -360,7 +360,7 @@ public override Task SetPropertyValueAsync( obj != null && captureDeviceProperty != null && captureDeviceProperty.IsPropertyValueValid(obj) && - captureGraphBuilder != null && + this.captureGraphBuilder != null && this.captureSource != null) { this.captureGraphBuilder.FindInterface( @@ -373,6 +373,7 @@ public override Task SetPropertyValueAsync( videoProcAmp.Get(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), out int _, out NativeMethods_DirectShow.VideoProcAmpFlags videoProcAmpFlags); videoProcAmp.Set(DirectShowProperty.FromVideoProcessingAmplifierProperty(property), (int)obj, videoProcAmpFlags); Marshal.ReleaseComObject(videoProcAmpObject); + return; } } diff --git a/samples/FlashCap.Properties/Program.cs b/samples/FlashCap.Properties/Program.cs index da576fc..0a6066b 100644 --- a/samples/FlashCap.Properties/Program.cs +++ b/samples/FlashCap.Properties/Program.cs @@ -70,15 +70,24 @@ private static async Task SetCameraPropertyAsync(CancellationToken ct) var brightnessProperty = captureDevice.Properties.Where(x => x.Key == VideoProcessingAmplifierProperty.Brightness).Select(x => x.Value).FirstOrDefault() ?? throw new Exception("Brightness is not supported with current device"); + var originalBrightnessValue = await captureDevice.GetPropertyValueAsync( + VideoProcessingAmplifierProperty.Brightness); + Console.WriteLine($"Current brightness property value is {originalBrightnessValue}"); + await captureDevice.SetPropertyValueAsync( VideoProcessingAmplifierProperty.Brightness, brightnessProperty.Min); - Console.WriteLine($"Brightness property value updated with {brightnessProperty.Min}"); + Console.WriteLine($"Brightness property value updated to {brightnessProperty.Min}"); var brightness = await captureDevice.GetPropertyValueAsync( VideoProcessingAmplifierProperty.Brightness); Console.WriteLine($"Brightness property value is {brightness}"); + await captureDevice.SetPropertyValueAsync( + VideoProcessingAmplifierProperty.Brightness, + originalBrightnessValue); + Console.WriteLine($"Brightness property value restored to {originalBrightnessValue}"); + return 0; }