diff --git a/Directory.Build.props b/Directory.Build.props
index cc24217..bc9df1e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,7 +18,7 @@
FlashCap
false
true
- $(NoWarn);CS1570;CS1591;CA1416;CS8981;NETSDK1215
+ $(NoWarn);CS1570;CS1591;CS8981;NETSDK1215
FlashCap
FlashCap
diff --git a/FlashCap.Core/CaptureDeviceDescriptor.cs b/FlashCap.Core/CaptureDeviceDescriptor.cs
index f2ea72a..589a85e 100644
--- a/FlashCap.Core/CaptureDeviceDescriptor.cs
+++ b/FlashCap.Core/CaptureDeviceDescriptor.cs
@@ -23,6 +23,7 @@ public enum DeviceTypes
DirectShow,
V4L2,
AVFoundation,
+ MediaFoundation,
}
public enum TranscodeFormats
diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs
index 8426cb0..47e0c31 100644
--- a/FlashCap.Core/CaptureDevices.cs
+++ b/FlashCap.Core/CaptureDevices.cs
@@ -13,11 +13,23 @@
using FlashCap.Internal;
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Runtime.Versioning;
namespace FlashCap;
+///
+/// By default, the following backends are considered for :
+///
+/// - (windows)
+/// - (windows)
+/// - MediaFoundationDevices (Windows 7 or greater) - Supported on net48, netstandard2.0 or greater, .NET 5.0 or greater
+/// - (linux)
+/// - (macOs)
+///
+///
public class CaptureDevices
{
protected readonly BufferPool DefaultBufferPool;
@@ -26,24 +38,37 @@ public CaptureDevices() :
this(new DefaultBufferPool())
{
}
-
- public CaptureDevices(BufferPool defaultBufferPool) =>
- this.DefaultBufferPool = defaultBufferPool;
- protected virtual IEnumerable OnEnumerateDescriptors() =>
- NativeMethods.CurrentPlatform switch
+ public CaptureDevices(BufferPool defaultBufferPool)
+ {
+ DefaultBufferPool = defaultBufferPool;
+ }
+
+ [RequiresUnreferencedCode("OnEnumerateDescriptors adds DirectShow which requires unreferenced code. Use platform-specific Devices directly.")]
+ protected virtual IEnumerable OnEnumerateDescriptors()
+ {
+ if (NativeMethods.IsWindows())
{
- NativeMethods.Platforms.Windows =>
- new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors().
- Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()),
- NativeMethods.Platforms.Linux =>
- new V4L2Devices().OnEnumerateDescriptors(),
- NativeMethods.Platforms.MacOS =>
- new AVFoundationDevices().OnEnumerateDescriptors(),
- _ =>
- ArrayEx.Empty(),
- };
+ var descriptors = new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors();
+ descriptors = descriptors.Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors());
+#if FLASHCAP_MEDIAFOUNDATION
+ if (NativeMethods.IsWindowsVersionAtLeast(6, 1))
+ descriptors = descriptors.Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors());
+#endif
+ return descriptors;
+ }
+ if (NativeMethods.IsLinux())
+ {
+ return new V4L2Devices().OnEnumerateDescriptors();
+ }
+ if (NativeMethods.IsMacOS())
+ {
+ return new AVFoundationDevices().OnEnumerateDescriptors();
+ }
+ return ArrayEx.Empty();
+ }
+ [RequiresUnreferencedCode("OnEnumerateDescriptors adds DirectShow which requires unreferenced code. Use platform-specific Devices directly.")]
internal IEnumerable InternalEnumerateDescriptors() =>
this.OnEnumerateDescriptors();
}
diff --git a/FlashCap.Core/Devices/AVFoundationDevice.cs b/FlashCap.Core/Devices/AVFoundationDevice.cs
index 8724472..27391b4 100644
--- a/FlashCap.Core/Devices/AVFoundationDevice.cs
+++ b/FlashCap.Core/Devices/AVFoundationDevice.cs
@@ -12,6 +12,7 @@
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using FlashCap.Internal;
@@ -23,6 +24,7 @@
namespace FlashCap.Devices;
+[SupportedOSPlatform("macos")]
public sealed class AVFoundationDevice : CaptureDevice
{
private readonly string uniqueID;
diff --git a/FlashCap.Core/Devices/AVFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/AVFoundationDeviceDescriptor.cs
index a270dee..b017fcd 100644
--- a/FlashCap.Core/Devices/AVFoundationDeviceDescriptor.cs
+++ b/FlashCap.Core/Devices/AVFoundationDeviceDescriptor.cs
@@ -8,11 +8,13 @@
//
////////////////////////////////////////////////////////////////////////////
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace FlashCap.Devices;
+[SupportedOSPlatform("macos")]
public sealed class AVFoundationDeviceDescriptor : CaptureDeviceDescriptor
{
private readonly string uniqueId;
diff --git a/FlashCap.Core/Devices/AVFoundationDevices.cs b/FlashCap.Core/Devices/AVFoundationDevices.cs
index 8d55537..09d0bee 100644
--- a/FlashCap.Core/Devices/AVFoundationDevices.cs
+++ b/FlashCap.Core/Devices/AVFoundationDevices.cs
@@ -10,7 +10,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using FlashCap.Internal;
using FlashCap.Utilities;
@@ -18,6 +20,7 @@
namespace FlashCap.Devices;
+[SupportedOSPlatform("macos")]
public sealed class AVFoundationDevices : CaptureDevices
{
public AVFoundationDevices() :
@@ -30,8 +33,14 @@ public AVFoundationDevices(BufferPool defaultBufferPool) :
{
}
+ [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This backend does not require unreferenced code")]
protected override IEnumerable OnEnumerateDescriptors()
{
+ if (!NativeMethods.IsMacOS())
+ {
+ throw new PlatformNotSupportedException("AVFoundation capture requires macOS.");
+ }
+
if (AVCaptureDevice.GetAuthorizationStatus(AVMediaType.Video) != AVAuthorizationStatus.Authorized)
{
TaskCompletionSource tcs = new();
diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs
index 345a3b7..c08c0c1 100644
--- a/FlashCap.Core/Devices/DirectShowDevice.cs
+++ b/FlashCap.Core/Devices/DirectShowDevice.cs
@@ -10,14 +10,19 @@
using FlashCap.Internal;
using System;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace FlashCap.Devices;
-
+#if NET6_0_OR_GREATER // Remove this when dropping net5.0. Its required because RequiresUnreferencedCode was not allowed on class level in net5.0
+[RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
+#endif
+[SupportedOSPlatform("windows")]
public sealed class DirectShowDevice :
CaptureDevice
{
diff --git a/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs b/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs
index 01f36a1..12294e6 100644
--- a/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs
+++ b/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs
@@ -7,11 +7,17 @@
//
////////////////////////////////////////////////////////////////////////////
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace FlashCap.Devices;
+#if NET6_0_OR_GREATER // Remove this when dropping net5.0. Its required because RequiresUnreferencedCode was not allowed on class level in net5.0
+[RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
+#endif
+[SupportedOSPlatform("windows")]
public sealed class DirectShowDeviceDescriptor : CaptureDeviceDescriptor
{
private readonly string devicePath;
diff --git a/FlashCap.Core/Devices/DirectShowDevices.cs b/FlashCap.Core/Devices/DirectShowDevices.cs
index b5d97fd..1db3153 100644
--- a/FlashCap.Core/Devices/DirectShowDevices.cs
+++ b/FlashCap.Core/Devices/DirectShowDevices.cs
@@ -10,10 +10,16 @@
using FlashCap.Internal;
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Runtime.Versioning;
namespace FlashCap.Devices;
+#if NET6_0_OR_GREATER // Remove this when dropping net5.0. Its required because RequiresUnreferencedCode was not allowed on class level in net5.0
+[RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
+#endif
+[SupportedOSPlatform("windows")]
public sealed class DirectShowDevices : CaptureDevices
{
public DirectShowDevices() :
diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs
new file mode 100644
index 0000000..2d5d557
--- /dev/null
+++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs
@@ -0,0 +1,405 @@
+#if FLASHCAP_MEDIAFOUNDATION
+////////////////////////////////////////////////////////////////////////////
+//
+// FlashCap - Independent camera capture library.
+// Copyright (c) Kouji Matsui (@kekyo@mi.kekyo.net)
+//
+// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
+//
+////////////////////////////////////////////////////////////////////////////
+
+using FlashCap.Internal;
+using System;
+using System.Runtime.ExceptionServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.Win32.Media.MediaFoundation;
+using FlashCap.Internal.MediaFoundation;
+using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_CONSTANTS;
+#if !NET9_0_OR_GREATER
+using Lock = System.Object;
+#endif
+
+namespace FlashCap.Devices;
+
+[SupportedOSPlatform("windows6.1")]
+public sealed class MediaFoundationDevice : CaptureDevice
+{
+ private readonly Lock sync = new();
+ private readonly string symbolicLink;
+ private readonly MediaFoundationInterop.FormatKey formatKey;
+
+ private FrameProcessor? frameProcessor;
+ private TranscodeFormats transcodeFormat;
+ private IntPtr bitmapHeader;
+ private byte[]? repackBuffer;
+ private CancellationTokenSource? stopSource;
+ private Task captureTask = Task.CompletedTask;
+ private Task interruptTask = Task.CompletedTask;
+ private unsafe IMFSourceReader* activeSourceReader;
+ private bool disposed;
+
+ internal MediaFoundationDevice(
+ string symbolicLink,
+ string name,
+ MediaFoundationInterop.FormatKey formatKey) :
+ base(symbolicLink, name)
+ {
+ this.symbolicLink = symbolicLink;
+ this.formatKey = formatKey;
+ }
+
+ protected override Task OnInitializeAsync(
+ VideoCharacteristics characteristics,
+ TranscodeFormats transcodeFormat,
+ FrameProcessor frameProcessor,
+ CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+ if (!NativeMethods.GetCompressionAndBitCount(
+ characteristics.PixelFormat, out var compression, out var bitCount))
+ {
+ throw new ArgumentException("FlashCap: Unsupported Media Foundation format.", nameof(characteristics));
+ }
+
+ this.Characteristics = characteristics;
+ this.transcodeFormat = transcodeFormat;
+ this.frameProcessor = frameProcessor;
+ this.bitmapHeader = NativeMethods.AllocateMemory(
+ new IntPtr(MarshalEx.SizeOf()));
+
+ unsafe
+ {
+ var header = (NativeMethods.BITMAPINFOHEADER*)this.bitmapHeader;
+ *header = default;
+ header->biSize = MarshalEx.SizeOf();
+ header->biWidth = characteristics.Width;
+ header->biHeight = characteristics.Height;
+ header->biPlanes = 1;
+ header->biBitCount = bitCount;
+ header->biCompression = compression;
+ header->biSizeImage = header->CalculateImageSize();
+ }
+ return Task.CompletedTask;
+ }
+
+ protected override async Task OnDisposeAsync()
+ {
+ if (this.disposed)
+ {
+ return;
+ }
+ this.disposed = true;
+
+ Exception? stopFailure = null;
+ try
+ {
+ await this.OnStopAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ stopFailure = exception;
+ }
+ finally
+ {
+ try
+ {
+ if (this.frameProcessor is not null)
+ {
+ await this.frameProcessor.DisposeAsync().ConfigureAwait(false);
+ this.frameProcessor = null;
+ }
+ }
+ finally
+ {
+ if (this.bitmapHeader != IntPtr.Zero)
+ {
+ NativeMethods.FreeMemory(this.bitmapHeader);
+ this.bitmapHeader = IntPtr.Zero;
+ }
+ }
+ }
+
+ if (stopFailure is not null)
+ {
+ ExceptionDispatchInfo.Capture(stopFailure).Throw();
+ }
+ }
+
+ protected override async Task OnStartAsync(CancellationToken ct)
+ {
+ if (this.disposed)
+ {
+ throw new ObjectDisposedException(nameof(MediaFoundationDevice));
+ }
+ Task previousCapture;
+ bool alreadyRunning;
+ lock (this.sync)
+ {
+ previousCapture = this.captureTask;
+ alreadyRunning = this.IsRunning &&
+ this.stopSource is { IsCancellationRequested: false };
+ }
+ if (alreadyRunning)
+ {
+ return;
+ }
+ await MediaFoundationHelpers.WaitAsync(previousCapture, ct).ConfigureAwait(false);
+
+ var startup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var stopSource = new CancellationTokenSource();
+ lock (this.sync)
+ {
+ this.stopSource?.Dispose();
+ this.stopSource = stopSource;
+ this.interruptTask = Task.CompletedTask;
+ this.captureTask = Task.Factory.StartNew(
+ () => this.Capture(startup, stopSource.Token),
+ CancellationToken.None,
+ TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
+ TaskScheduler.Default);
+ }
+
+ try
+ {
+ await MediaFoundationHelpers.WaitAsync(startup.Task, ct).ConfigureAwait(false);
+ }
+ catch
+ {
+ await this.OnStopAsync(CancellationToken.None).ConfigureAwait(false);
+ throw;
+ }
+ }
+
+ protected override async Task OnStopAsync(CancellationToken ct)
+ {
+ this.PrepareStop(out var captureTask, out var interruptTask);
+
+ try
+ {
+ await MediaFoundationHelpers.WaitAsync(Task.WhenAll(interruptTask, captureTask), ct).
+ ConfigureAwait(false);
+ }
+ finally
+ {
+ if (captureTask.IsCompleted)
+ {
+ lock (this.sync)
+ {
+ if (ReferenceEquals(this.captureTask, captureTask))
+ {
+ this.stopSource?.Dispose();
+ this.stopSource = null;
+ this.captureTask = Task.CompletedTask;
+ this.interruptTask = Task.CompletedTask;
+ }
+ }
+ }
+ }
+ }
+
+ private unsafe void PrepareStop(out Task captureTask, out Task interruptTask)
+ {
+ lock (this.sync)
+ {
+ this.stopSource?.Cancel();
+ captureTask = this.captureTask;
+ if (!captureTask.IsCompleted && this.interruptTask.IsCompleted &&
+ this.activeSourceReader is not null)
+ {
+ var sourceReader = this.activeSourceReader;
+ this.interruptTask = Task.Factory.StartNew(
+ () => Interrupt(sourceReader),
+ CancellationToken.None,
+ TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
+ TaskScheduler.Default);
+ }
+ interruptTask = this.interruptTask;
+ }
+ }
+
+ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stopToken)
+ {
+ CaptureSession? session = null;
+ bool initialized = false;
+ bool startupCompleted = false;
+ try
+ {
+ MediaFoundationInterop.Initialize();
+ initialized = true;
+
+ session = CaptureSession.Open(this.symbolicLink, this.formatKey);
+ stopToken.ThrowIfCancellationRequested();
+
+ lock (this.sync)
+ {
+ this.activeSourceReader = session.SourceReader;
+ }
+ this.IsRunning = true;
+ startupCompleted = true;
+ startup.TrySetResult(true);
+ session.ReadFrames(stopToken, this.OnFrame);
+ }
+ catch (OperationCanceledException) when (stopToken.IsCancellationRequested)
+ {
+ if (!startupCompleted)
+ {
+ startup.TrySetCanceled(stopToken);
+ }
+ }
+ catch (Exception exception)
+ {
+ if (!startupCompleted)
+ {
+ startup.TrySetException(exception);
+ }
+ else
+ {
+ MediaFoundationHelpers.TraceFailure("capture", exception);
+ }
+ }
+ finally
+ {
+ this.IsRunning = false;
+ Task pendingInterrupt;
+ lock (this.sync)
+ {
+ this.activeSourceReader = null;
+ pendingInterrupt = this.interruptTask;
+ }
+ try
+ {
+ pendingInterrupt.GetAwaiter().GetResult();
+ }
+ catch (Exception exception)
+ {
+ MediaFoundationHelpers.TraceFailure("capture interruption", exception);
+ }
+
+ session?.Dispose();
+ if (initialized)
+ {
+ MediaFoundationInterop.Uninitialize();
+ }
+ if (!startupCompleted)
+ {
+ startup.TrySetException(new InvalidOperationException(
+ "FlashCap: Media Foundation capture ended during startup."));
+ }
+ }
+ }
+
+ private unsafe void OnFrame(
+ byte* data,
+ int length,
+ int? defaultStride,
+ long timestampMicroseconds,
+ long frameIndex)
+ {
+ var frame = this.NormalizeFrame(data, length, defaultStride);
+ try
+ {
+ if (this.frameProcessor is null)
+ {
+ throw new InvalidOperationException("FlashCap: The frame processor is not initialized.");
+ }
+ if (frame.Pointer != IntPtr.Zero)
+ {
+ this.frameProcessor.OnFrameArrived(
+ this, frame.Pointer, frame.Length, timestampMicroseconds, frameIndex);
+ }
+ else
+ {
+ fixed (byte* repacked = this.repackBuffer!)
+ {
+ this.frameProcessor.OnFrameArrived(
+ this, (IntPtr)repacked, frame.Length, timestampMicroseconds, frameIndex);
+ }
+ }
+ }
+ catch (Exception exception)
+ {
+ MediaFoundationHelpers.TraceFailure("frame callback", exception);
+ }
+ }
+
+ private unsafe FrameMemory NormalizeFrame(byte* data, int length, int? defaultStride)
+ {
+ var format = this.Characteristics.PixelFormat;
+ if (format == PixelFormats.JPEG)
+ {
+ return new FrameMemory((IntPtr)data, length);
+ }
+
+ MediaFoundationInterop.FrameLayout layout;
+ try
+ {
+ layout = MediaFoundationInterop.GetFrameLayout(
+ format,
+ this.Characteristics.Width,
+ this.Characteristics.Height,
+ defaultStride,
+ length);
+ }
+ catch (ArgumentException exception)
+ {
+ throw new InvalidOperationException(
+ "FlashCap: Media Foundation returned an invalid frame buffer.", exception);
+ }
+
+ if (layout.SourceStride == layout.TargetStride &&
+ (!(format is PixelFormats.RGB15 or PixelFormats.RGB16 or
+ PixelFormats.RGB24 or PixelFormats.RGB32 or PixelFormats.ARGB32) || layout.BottomUp))
+ {
+ return new FrameMemory((IntPtr)data, layout.TargetLength);
+ }
+
+ if (this.repackBuffer is null || this.repackBuffer.Length < layout.TargetLength)
+ {
+ this.repackBuffer = new byte[layout.TargetLength];
+ }
+ var managedBuffer = this.repackBuffer;
+ var reverseRows = !layout.BottomUp &&
+ format is PixelFormats.RGB15 or PixelFormats.RGB16 or
+ PixelFormats.RGB24 or PixelFormats.RGB32 or PixelFormats.ARGB32;
+ MediaFoundationInterop.RepackFrame(
+ new ReadOnlySpan(data, length),
+ managedBuffer.AsSpan(0, layout.TargetLength),
+ layout,
+ reverseRows);
+ return new FrameMemory(IntPtr.Zero, layout.TargetLength);
+ }
+
+ private static unsafe void Interrupt(IMFSourceReader* reader)
+ {
+ MediaFoundationInterop.Initialize();
+ try
+ {
+ if (reader is not null)
+ {
+ MediaFoundationHelpers.ThrowIfFailed(
+ reader->Flush(unchecked((uint)MF_SOURCE_READER_ALL_STREAMS)),
+ "IMFSourceReader.Flush");
+ }
+ }
+ finally
+ {
+ MediaFoundationInterop.Uninitialize();
+ }
+ }
+
+ protected override void OnCapture(
+ IntPtr pData,
+ int size,
+ long timestampMicroseconds,
+ long frameIndex,
+ PixelBuffer buffer)
+ {
+ buffer.CopyIn(this.bitmapHeader, pData, size, timestampMicroseconds, frameIndex, this.transcodeFormat);
+ }
+
+ private readonly record struct FrameMemory(IntPtr Pointer, int Length);
+}
+#endif
diff --git a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs
new file mode 100644
index 0000000..d745486
--- /dev/null
+++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs
@@ -0,0 +1,63 @@
+#if FLASHCAP_MEDIAFOUNDATION
+////////////////////////////////////////////////////////////////////////////
+//
+// FlashCap - Independent camera capture library.
+// Copyright (c) Kouji Matsui (@kekyo@mi.kekyo.net)
+//
+// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Threading;
+using System.Threading.Tasks;
+using FlashCap.Internal.MediaFoundation;
+
+namespace FlashCap.Devices;
+
+[SupportedOSPlatform("windows6.1")]
+public sealed class MediaFoundationDeviceDescriptor : CaptureDeviceDescriptor
+{
+ private readonly string symbolicLink;
+ private readonly IReadOnlyDictionary characteristicToFormatLookup;
+
+ internal MediaFoundationDeviceDescriptor(
+ string symbolicLink,
+ string name,
+ string description,
+ IReadOnlyDictionary characteristicToFormatLookup,
+ BufferPool defaultBufferPool) :
+ base(name, description, characteristicToFormatLookup.Keys.ToArray(), defaultBufferPool)
+ {
+ this.symbolicLink = symbolicLink;
+ this.characteristicToFormatLookup = characteristicToFormatLookup;
+ }
+
+ public override object Identity => this.symbolicLink;
+
+ public override DeviceTypes DeviceType => DeviceTypes.MediaFoundation;
+
+ protected override Task OnOpenWithFrameProcessorAsync(
+ VideoCharacteristics characteristics,
+ TranscodeFormats transcodeFormat,
+ FrameProcessor frameProcessor,
+ CancellationToken ct)
+ {
+ if (!this.characteristicToFormatLookup.TryGetValue(characteristics, out var formatKey))
+ {
+ throw new System.ArgumentException(
+ "FlashCap: The selected Media Foundation format is not available.",
+ nameof(characteristics));
+ }
+
+ return this.InternalOnOpenWithFrameProcessorAsync(
+ new MediaFoundationDevice(this.symbolicLink, this.Name, formatKey),
+ characteristics,
+ transcodeFormat,
+ frameProcessor,
+ ct);
+ }
+}
+#endif
diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs
new file mode 100644
index 0000000..bcb0287
--- /dev/null
+++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs
@@ -0,0 +1,62 @@
+#if FLASHCAP_MEDIAFOUNDATION
+////////////////////////////////////////////////////////////////////////////
+//
+// FlashCap - Independent camera capture library.
+// Copyright (c) Kouji Matsui (@kekyo@mi.kekyo.net)
+//
+// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Threading;
+using System.Threading.Tasks;
+using FlashCap.Internal;
+using FlashCap.Internal.MediaFoundation;
+
+namespace FlashCap.Devices;
+
+[SupportedOSPlatform("windows6.1")]
+public sealed class MediaFoundationDevices(BufferPool defaultBufferPool) : CaptureDevices(defaultBufferPool)
+{
+ public MediaFoundationDevices() :
+ this(new DefaultBufferPool())
+ {
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This backend does not require unreferenced code")]
+ protected override IEnumerable OnEnumerateDescriptors()
+ {
+ if (!NativeMethods.IsWindowsVersionAtLeast(6, 1))
+ {
+ throw new PlatformNotSupportedException("Media Foundation capture requires Windows 7 or later.");
+ }
+
+ try
+ {
+ return Task.Factory.StartNew(
+ () => MediaFoundationInterop.EnumerateDevices()
+ .Select(device => (CaptureDeviceDescriptor)new MediaFoundationDeviceDescriptor(
+ device.SymbolicLink,
+ device.Name,
+ $"{device.Name} (Media Foundation)",
+ device.Formats,
+ this.DefaultBufferPool))
+ .ToArray(),
+ CancellationToken.None,
+ TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
+ TaskScheduler.Default).GetAwaiter().GetResult();
+ }
+ catch (Exception exception)
+ {
+ MediaFoundationHelpers.TraceFailure("device discovery", exception);
+ return [];
+ }
+ }
+
+}
+#endif
diff --git a/FlashCap.Core/Devices/V4L2Device.cs b/FlashCap.Core/Devices/V4L2Device.cs
index c6a23d9..05a9220 100644
--- a/FlashCap.Core/Devices/V4L2Device.cs
+++ b/FlashCap.Core/Devices/V4L2Device.cs
@@ -12,6 +12,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@@ -20,6 +21,7 @@
namespace FlashCap.Devices;
+[SupportedOSPlatform("linux")]
public sealed class V4L2Device : CaptureDevice
{
private const int BufferCount = 2;
diff --git a/FlashCap.Core/Devices/V4L2DeviceDescriptor.cs b/FlashCap.Core/Devices/V4L2DeviceDescriptor.cs
index 6049751..f4f9ca4 100644
--- a/FlashCap.Core/Devices/V4L2DeviceDescriptor.cs
+++ b/FlashCap.Core/Devices/V4L2DeviceDescriptor.cs
@@ -7,11 +7,13 @@
//
////////////////////////////////////////////////////////////////////////////
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace FlashCap.Devices;
+[SupportedOSPlatform("linux")]
public sealed class V4L2DeviceDescriptor : CaptureDeviceDescriptor
{
private readonly string devicePath;
diff --git a/FlashCap.Core/Devices/V4L2Devices.cs b/FlashCap.Core/Devices/V4L2Devices.cs
index b99a772..287d803 100644
--- a/FlashCap.Core/Devices/V4L2Devices.cs
+++ b/FlashCap.Core/Devices/V4L2Devices.cs
@@ -11,8 +11,10 @@
using FlashCap.Internal;
using FlashCap.Utilities;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
+using System.Runtime.Versioning;
using System.Text;
using static FlashCap.Internal.NativeMethods_V4L2;
@@ -20,6 +22,7 @@
namespace FlashCap.Devices;
+[SupportedOSPlatform("linux")]
public sealed class V4L2Devices : CaptureDevices
{
public V4L2Devices() :
@@ -187,8 +190,15 @@ private static string ToString(byte[] data)
return str;
}
- protected override IEnumerable OnEnumerateDescriptors() =>
- Directory.GetFiles("/dev", "video*").
+ [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This backend does not require unreferenced code")]
+ protected override IEnumerable OnEnumerateDescriptors()
+ {
+ if (!NativeMethods.IsLinux())
+ {
+ throw new PlatformNotSupportedException("V4L2 capture requires Linux.");
+ }
+
+ return Directory.GetFiles("/dev", "video*").
Collect(devicePath =>
{
if (open(devicePath, OPENBITS.O_RDWR) is { } fd && fd >= 0)
@@ -231,4 +241,5 @@ protected override IEnumerable OnEnumerateDescriptors()
return null;
}
});
+ }
}
diff --git a/FlashCap.Core/Devices/VideoForWindowsDevice.cs b/FlashCap.Core/Devices/VideoForWindowsDevice.cs
index 1ea729e..4607f8b 100644
--- a/FlashCap.Core/Devices/VideoForWindowsDevice.cs
+++ b/FlashCap.Core/Devices/VideoForWindowsDevice.cs
@@ -12,11 +12,13 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using System.Threading;
namespace FlashCap.Devices;
+[SupportedOSPlatform("windows")]
public sealed class VideoForWindowsDevice : CaptureDevice
{
private readonly TimestampCounter counter = new();
diff --git a/FlashCap.Core/Devices/VideoForWindowsDeviceDescriptor.cs b/FlashCap.Core/Devices/VideoForWindowsDeviceDescriptor.cs
index 240b185..f126cd7 100644
--- a/FlashCap.Core/Devices/VideoForWindowsDeviceDescriptor.cs
+++ b/FlashCap.Core/Devices/VideoForWindowsDeviceDescriptor.cs
@@ -9,9 +9,11 @@
using System.Threading;
using System.Threading.Tasks;
+using System.Runtime.Versioning;
namespace FlashCap.Devices;
+[SupportedOSPlatform("windows")]
public sealed class VideoForWindowsDeviceDescriptor : CaptureDeviceDescriptor
{
private readonly int deviceIndex;
diff --git a/FlashCap.Core/Devices/VideoForWindowsDevices.cs b/FlashCap.Core/Devices/VideoForWindowsDevices.cs
index 88992a7..27627f3 100644
--- a/FlashCap.Core/Devices/VideoForWindowsDevices.cs
+++ b/FlashCap.Core/Devices/VideoForWindowsDevices.cs
@@ -9,11 +9,14 @@
using FlashCap.Internal;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
+using System.Runtime.Versioning;
namespace FlashCap.Devices;
+[SupportedOSPlatform("windows")]
public sealed class VideoForWindowsDevices : CaptureDevices
{
public VideoForWindowsDevices() :
@@ -26,6 +29,7 @@ public VideoForWindowsDevices(BufferPool defaultBufferPool) :
{
}
+ [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This backend does not require unreferenced code")]
protected override IEnumerable OnEnumerateDescriptors() =>
Enumerable.Range(0, NativeMethods_VideoForWindows.MaxVideoForWindowsDevices).
Collect(index =>
diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj
index c59cbb8..12c8927 100644
--- a/FlashCap.Core/FlashCap.Core.csproj
+++ b/FlashCap.Core/FlashCap.Core.csproj
@@ -5,12 +5,24 @@
True
$(NoWarn);CS0649
true
+ true
+
+
+
+ $(DefineConstants);FLASHCAP_MEDIAFOUNDATION
+ true
+
+
+ runtime; build; native; contentfiles; analyzers
+
+
+
@@ -20,6 +32,14 @@
+
+
+
+
+
+
+
+
diff --git a/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs b/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs
index 06189e1..1ee2a11 100644
--- a/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs
+++ b/FlashCap.Core/Internal/AVFoundation/AVCaptureVideoDataOutput.cs
@@ -11,6 +11,7 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using FlashCap.Devices;
using static FlashCap.Internal.NativeMethods_AVFoundation;
@@ -18,6 +19,7 @@ namespace FlashCap.Internal.AVFoundation;
partial class LibAVFoundation
{
+ [SupportedOSPlatform("macos")]
public sealed class AVCaptureVideoDataOutput : AVCaptureOutput
{
private AVCaptureVideoDataOutputSampleBuffer.CaptureOutputDidOutputSampleBuffer? callbackDelegate;
diff --git a/FlashCap.Core/Internal/BackwardCompat.cs b/FlashCap.Core/Internal/BackwardCompat.cs
index 2c2e578..50a0b4a 100644
--- a/FlashCap.Core/Internal/BackwardCompat.cs
+++ b/FlashCap.Core/Internal/BackwardCompat.cs
@@ -174,7 +174,7 @@ public void SetApartmentState(ApartmentState state) =>
private void EntryPoint()
{
- if (NativeMethods.CurrentPlatform == NativeMethods.Platforms.Windows)
+ if (NativeMethods.IsWindows())
{
switch (this.state)
{
@@ -329,3 +329,201 @@ public static void For(int fromInclusive, int toExclusive, Action body)
}
}
#endif
+#if !NET5_0_OR_GREATER
+namespace System.Runtime.CompilerServices
+{
+ internal static class IsExternalInit
+ {
+ }
+}
+
+namespace System.Runtime.Versioning
+{
+ [AttributeUsage(
+ AttributeTargets.Assembly |
+ AttributeTargets.Class |
+ AttributeTargets.Constructor |
+ AttributeTargets.Delegate |
+ AttributeTargets.Enum |
+ AttributeTargets.Event |
+ AttributeTargets.Field |
+ AttributeTargets.Interface |
+ AttributeTargets.Method |
+ AttributeTargets.Module |
+ AttributeTargets.Property |
+ AttributeTargets.Struct,
+ AllowMultiple = true,
+ Inherited = false)]
+ internal sealed class SupportedOSPlatformAttribute : Attribute
+ {
+ public SupportedOSPlatformAttribute(string platformName)
+ {
+ PlatformName = platformName;
+ }
+ public string PlatformName { get; }
+ }
+}
+namespace System.Diagnostics.CodeAnalysis
+{
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
+ internal sealed class RequiresUnreferencedCodeAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class
+ /// with the specified message.
+ ///
+ ///
+ /// A message that contains information about the usage of unreferenced code.
+ ///
+ public RequiresUnreferencedCodeAttribute(string message)
+ {
+ Message = message;
+ }
+
+ ///
+ /// When set to true, indicates that the annotation should not apply to static members.
+ ///
+ public bool ExcludeStatics { get; set; }
+
+ ///
+ /// Gets a message that contains information about the usage of unreferenced code.
+ ///
+ public string Message { get; }
+
+ ///
+ /// Gets or sets an optional URL that contains more information about the method,
+ /// why it requires unreferenced code, and what options a consumer has to deal with it.
+ ///
+ public string? Url { get; set; }
+ }
+ [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
+ internal sealed class UnconditionalSuppressMessageAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the
+ /// class, specifying the category of the tool and the identifier for an analysis rule.
+ ///
+ /// The category for the attribute.
+ /// The identifier of the analysis rule the attribute applies to.
+ public UnconditionalSuppressMessageAttribute(string category, string checkId)
+ {
+ Category = category;
+ CheckId = checkId;
+ }
+
+ ///
+ /// Gets the category identifying the classification of the attribute.
+ ///
+ ///
+ /// The property describes the tool or tool analysis category
+ /// for which a message suppression attribute applies.
+ ///
+ public string Category { get; }
+
+ ///
+ /// Gets the identifier of the analysis tool rule to be suppressed.
+ ///
+ ///
+ /// Concatenated together, the and
+ /// properties form a unique check identifier.
+ ///
+ public string CheckId { get; }
+
+ ///
+ /// Gets or sets the scope of the code that is relevant for the attribute.
+ ///
+ ///
+ /// The Scope property is an optional argument that specifies the metadata scope for which
+ /// the attribute is relevant.
+ ///
+ public string? Scope { get; set; }
+
+ ///
+ /// Gets or sets a fully qualified path that represents the target of the attribute.
+ ///
+ ///
+ /// The property is an optional argument identifying the analysis target
+ /// of the attribute. An example value is "System.IO.Stream.ctor():System.Void".
+ /// Because it is fully qualified, it can be long, particularly for targets such as parameters.
+ /// The analysis tool user interface should be capable of automatically formatting the parameter.
+ ///
+ public string? Target { get; set; }
+
+ ///
+ /// Gets or sets an optional argument expanding on exclusion criteria.
+ ///
+ ///
+ /// The property is an optional argument that specifies additional
+ /// exclusion where the literal metadata target is not sufficiently precise. For example,
+ /// the cannot be applied within a method,
+ /// and it may be desirable to suppress a violation against a statement in the method that will
+ /// give a rule violation, but not against all statements in the method.
+ ///
+ public string? MessageId { get; set; }
+
+ ///
+ /// Gets or sets the justification for suppressing the code analysis message.
+ ///
+ public string? Justification { get; set; }
+ }
+}
+#endif
+
+#if !NET6_0_OR_GREATER
+namespace System.Runtime.Versioning
+{
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
+ internal sealed class SupportedOSPlatformGuardAttribute : Attribute
+ {
+ public SupportedOSPlatformGuardAttribute(string platformName)
+ {
+ PlatformName = platformName;
+ }
+ public string PlatformName { get; }
+ }
+}
+#endif
+
+#if !NET7_0_OR_GREATER
+namespace System.Diagnostics.CodeAnalysis
+{
+ ///
+ /// Indicates that the specified method requires the ability to generate new code at runtime,
+ /// for example through .
+ ///
+ ///
+ /// This allows tools to understand which methods are unsafe to call when compiling ahead of time.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
+ internal sealed class RequiresDynamicCodeAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class
+ /// with the specified message.
+ ///
+ ///
+ /// A message that contains information about the usage of dynamic code.
+ ///
+ public RequiresDynamicCodeAttribute(string message)
+ {
+ Message = message;
+ }
+
+ ///
+ /// When set to true, indicates that the annotation should not apply to static members.
+ ///
+ public bool ExcludeStatics { get; set; }
+
+ ///
+ /// Gets a message that contains information about the usage of dynamic code.
+ ///
+ public string Message { get; }
+
+ ///
+ /// Gets or sets an optional URL that contains more information about the method,
+ /// why it requires dynamic code, and what options a consumer has to deal with it.
+ ///
+ public string? Url { get; set; }
+ }
+}
+#endif
diff --git a/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs b/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs
index c78c7ba..5d5d863 100644
--- a/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs
+++ b/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs
@@ -14,11 +14,13 @@
using System.Diagnostics;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
namespace FlashCap.Internal;
+[SupportedOSPlatform("windows")]
internal sealed class IndependentSingleApartmentContext :
SynchronizationContext, IDisposable
{
@@ -113,7 +115,7 @@ public void Wait()
public IndependentSingleApartmentContext()
{
- Debug.Assert(NativeMethods.CurrentPlatform == NativeMethods.Platforms.Windows);
+ Debug.Assert(NativeMethods.IsWindows());
this.thread = new(this.ThreadEntry);
this.thread.IsBackground = true;
diff --git a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs
new file mode 100644
index 0000000..dcfbff0
--- /dev/null
+++ b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs
@@ -0,0 +1,236 @@
+#if FLASHCAP_MEDIAFOUNDATION
+////////////////////////////////////////////////////////////////////////////
+//
+// FlashCap - Independent camera capture library.
+// Copyright (c) Kouji Matsui (@kekyo@mi.kekyo.net)
+//
+// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
+//
+////////////////////////////////////////////////////////////////////////////
+using System;
+using System.Runtime.Versioning;
+using System.Threading;
+using Windows.Win32;
+using Windows.Win32.Media.MediaFoundation;
+using static FlashCap.Internal.MediaFoundation.MediaFoundationInterop;
+
+namespace FlashCap.Internal.MediaFoundation;
+
+[SupportedOSPlatform("windows6.1")]
+internal sealed unsafe class CaptureSession : IDisposable
+{
+ private IMFActivate* activate;
+ private IMFMediaSource* mediaSource;
+ private IMFSourceReader* reader;
+ private int? defaultStride;
+
+ private CaptureSession()
+ {
+ }
+
+ internal IMFSourceReader* SourceReader => this.reader;
+
+ internal static CaptureSession Open(string symbolicLink, FormatKey formatKey)
+ {
+ var session = new CaptureSession();
+ try
+ {
+ session.activate = FindActivate(symbolicLink);
+ session.mediaSource = ActivateMediaSource(session.activate);
+ session.reader = CreateSourceReader(session.mediaSource);
+ session.defaultStride = ConfigureReader(session.reader, formatKey);
+ return session;
+ }
+ catch
+ {
+ session.Dispose();
+ throw;
+ }
+ }
+
+ internal void ReadFrames(CancellationToken stopToken, FrameHandler frameHandler)
+ {
+ long? firstTimestamp = null;
+ long frameIndex = 0;
+ while (!stopToken.IsCancellationRequested)
+ {
+ uint flags = 0;
+ long timestamp = 0;
+ IMFSample* sample = null;
+ var result = this.reader->ReadSample(
+ VideoStreamIndex,
+ 0,
+ null,
+ &flags,
+ ×tamp,
+ &sample);
+ if (result.Failed)
+ {
+ if (stopToken.IsCancellationRequested)
+ {
+ break;
+ }
+ MediaFoundationHelpers.ThrowIfFailed(result, "IMFSourceReader.ReadSample");
+ }
+
+ try
+ {
+ if (stopToken.IsCancellationRequested)
+ {
+ break;
+ }
+ var readerFlags = (MF_SOURCE_READER_FLAG)flags;
+ if ((readerFlags & (MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_ERROR |
+ MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_ENDOFSTREAM |
+ MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED |
+ MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)) != 0)
+ {
+ throw new InvalidOperationException(
+ $"FlashCap: Media Foundation capture stream changed state ({readerFlags}).");
+ }
+ if (sample is null || (readerFlags & MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_STREAMTICK) != 0)
+ {
+ continue;
+ }
+
+ firstTimestamp ??= timestamp;
+ ProcessSample(
+ sample,
+ this.defaultStride,
+ Math.Max(0, timestamp - firstTimestamp.Value) / 10,
+ frameIndex++,
+ frameHandler);
+ }
+ finally
+ {
+ Release(sample);
+ }
+ }
+ }
+
+ private static int? ConfigureReader(IMFSourceReader* reader, FormatKey formatKey)
+ {
+ MediaFoundationHelpers.ThrowIfFailed(
+ reader->SetStreamSelection(unchecked((uint)MF_SOURCE_READER_CONSTANTS.MF_SOURCE_READER_ALL_STREAMS), false),
+ "IMFSourceReader.SetStreamSelection(all)");
+ MediaFoundationHelpers.ThrowIfFailed(
+ reader->SetStreamSelection(VideoStreamIndex, true),
+ "IMFSourceReader.SetStreamSelection(video)");
+
+ IMFMediaType* mediaType = null;
+ try
+ {
+ MediaFoundationHelpers.ThrowIfFailed(
+ reader->GetNativeMediaType(VideoStreamIndex, formatKey.MediaTypeIndex, &mediaType),
+ "IMFSourceReader.GetNativeMediaType");
+ if (mediaType is null ||
+ !TryCreateFormat(mediaType, formatKey.MediaTypeIndex, out var selected) ||
+ selected.Key != formatKey)
+ {
+ throw new InvalidOperationException(
+ "FlashCap: The selected Media Foundation format is no longer available.");
+ }
+
+ MediaFoundationHelpers.ThrowIfFailed(
+ reader->SetCurrentMediaType(VideoStreamIndex, mediaType),
+ "IMFSourceReader.SetCurrentMediaType");
+ return mediaType->GetUINT32(in PInvoke.MF_MT_DEFAULT_STRIDE, out var stride).Succeeded ?
+ unchecked((int)stride) : null;
+ }
+ finally
+ {
+ Release(mediaType);
+ }
+ }
+
+ private static void ProcessSample(
+ IMFSample* sample,
+ int? defaultStride,
+ long timestampMicroseconds,
+ long frameIndex,
+ FrameHandler frameHandler)
+ {
+ IMFMediaBuffer* buffer = null;
+ MediaFoundationHelpers.ThrowIfFailed(
+ sample->ConvertToContiguousBuffer(&buffer),
+ "IMFSample.ConvertToContiguousBuffer");
+ if (buffer is null)
+ {
+ throw new InvalidOperationException("FlashCap: Media Foundation returned no sample buffer.");
+ }
+
+ byte* data = null;
+ bool locked = false;
+ try
+ {
+ uint currentLength = 0;
+ MediaFoundationHelpers.ThrowIfFailed(buffer->Lock(&data, null, ¤tLength), "IMFMediaBuffer.Lock");
+ locked = true;
+ if (data is null || currentLength == 0 || currentLength > int.MaxValue)
+ {
+ throw new InvalidOperationException(
+ "FlashCap: Media Foundation returned an invalid frame buffer.");
+ }
+
+ frameHandler(
+ data,
+ checked((int)currentLength),
+ defaultStride,
+ timestampMicroseconds,
+ frameIndex);
+ }
+ finally
+ {
+ if (locked)
+ {
+ _ = buffer->Unlock();
+ }
+ Release(buffer);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (this.reader is null && this.mediaSource is not null)
+ {
+ _ = this.mediaSource->Shutdown();
+ }
+ Release(this.reader);
+ this.reader = null;
+ if (this.activate is not null)
+ {
+ _ = this.activate->ShutdownObject();
+ }
+ Release(this.mediaSource);
+ this.mediaSource = null;
+ Release(this.activate);
+ this.activate = null;
+ }
+
+ internal static IMFActivate* FindActivate(string symbolicLink)
+ {
+ IMFActivate** devices = null;
+ uint count = 0;
+ try
+ {
+ devices = EnumerateDeviceSources(out count);
+ for (uint index = 0; index < count; index++)
+ {
+ var activate = devices[index];
+ if (activate is not null && string.Equals(
+ GetAllocatedString(activate, in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim(),
+ symbolicLink, StringComparison.OrdinalIgnoreCase))
+ {
+ devices[index] = null;
+ return activate;
+ }
+ }
+ }
+ finally
+ {
+ FreeActivateArray(devices, count);
+ }
+ throw new InvalidOperationException("FlashCap: The Media Foundation device is no longer available.");
+ }
+}
+#endif
diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs
new file mode 100644
index 0000000..98d1903
--- /dev/null
+++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs
@@ -0,0 +1,55 @@
+#if FLASHCAP_MEDIAFOUNDATION
+////////////////////////////////////////////////////////////////////////////
+//
+// FlashCap - Independent camera capture library.
+// Copyright (c) Kouji Matsui (@kekyo@mi.kekyo.net)
+//
+// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.Win32.Foundation;
+
+namespace FlashCap.Internal.MediaFoundation;
+
+internal static class MediaFoundationHelpers
+{
+ internal static void ThrowIfFailed(HRESULT result, string operation)
+ {
+ if (result.Failed)
+ {
+ throw new InvalidOperationException(
+ $"FlashCap: {operation} failed (HRESULT=0x{unchecked((uint)result.Value):X8}).");
+ }
+ }
+
+ internal static void TraceFailure(string operation, Exception exception) =>
+ Trace.WriteLine($"FlashCap: Media Foundation {operation} failed: {exception}");
+
+ internal static async Task WaitAsync(Task task, CancellationToken ct)
+ {
+#if NET6_0_OR_GREATER
+ await task.WaitAsync(ct).ConfigureAwait(false);
+#else
+ if (task.IsCompleted)
+ {
+ await task.ConfigureAwait(false);
+ return;
+ }
+
+ var cancellation = new TaskCompletionSource(
+ TaskCreationOptions.RunContinuationsAsynchronously);
+ using var registration = ct.Register(() => cancellation.TrySetResult(true));
+ if (await Task.WhenAny(task, cancellation.Task).ConfigureAwait(false) != task)
+ {
+ ct.ThrowIfCancellationRequested();
+ }
+ await task.ConfigureAwait(false);
+#endif
+ }
+}
+#endif
diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs
new file mode 100644
index 0000000..15d0155
--- /dev/null
+++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs
@@ -0,0 +1,408 @@
+#if FLASHCAP_MEDIAFOUNDATION
+////////////////////////////////////////////////////////////////////////////
+//
+// FlashCap - Independent camera capture library.
+// Copyright (c) Kouji Matsui (@kekyo@mi.kekyo.net)
+//
+// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
+//
+////////////////////////////////////////////////////////////////////////////
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Media.MediaFoundation;
+using Windows.Win32.System.Com;
+using FlashCap.Utilities;
+using static FlashCap.Internal.MediaFoundation.MediaFoundationHelpers;
+
+namespace FlashCap.Internal.MediaFoundation;
+
+[SupportedOSPlatform("windows6.1")]
+internal static unsafe class MediaFoundationInterop
+{
+ internal const uint VideoStreamIndex = 0;
+
+ internal readonly record struct FormatKey(
+ uint MediaTypeIndex,
+ Guid Subtype,
+ int Width,
+ int Height,
+ Fraction FrameRate);
+
+ internal readonly record struct Format(
+ FormatKey Key,
+ VideoCharacteristics Characteristics);
+
+ internal readonly record struct DeviceInfo(
+ string SymbolicLink,
+ string Name,
+ IReadOnlyDictionary Formats);
+
+ internal readonly record struct FrameLayout(
+ int RowLength,
+ int Rows,
+ int SourceStride,
+ int TargetStride,
+ bool BottomUp)
+ {
+ internal int TargetLength => checked(this.TargetStride * this.Rows);
+ }
+
+ internal delegate void FrameHandler(
+ byte* data,
+ int length,
+ int? defaultStride,
+ long timestampMicroseconds,
+ long frameIndex);
+
+ ///
+ /// Initializes COM as MTA and starts Media Foundation on the current thread.
+ /// After this method succeeds, call in a finally block on the same thread.
+ ///
+ internal static void Initialize()
+ {
+ var result = PInvoke.CoInitializeEx(COINIT.COINIT_MULTITHREADED);
+ ThrowIfFailed(result, nameof(PInvoke.CoInitializeEx));
+
+ result = PInvoke.MFStartup(PInvoke.MF_VERSION, PInvoke.MFSTARTUP_FULL);
+ if (result.Failed)
+ {
+ PInvoke.CoUninitialize();
+ ThrowIfFailed(result, nameof(PInvoke.MFStartup));
+ }
+ }
+
+ internal static void Uninitialize()
+ {
+ _ = PInvoke.MFShutdown();
+ PInvoke.CoUninitialize();
+ }
+
+ internal static IMFAttributes* CreateVideoCaptureAttributes()
+ {
+ IMFAttributes* attributes = null;
+ ThrowIfFailed(PInvoke.MFCreateAttributes(&attributes, 1), nameof(PInvoke.MFCreateAttributes));
+ try
+ {
+ ThrowIfFailed(
+ attributes->SetGUID(
+ in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
+ in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID),
+ "IMFAttributes.SetGUID");
+ return attributes;
+ }
+ catch
+ {
+ Release(attributes);
+ throw;
+ }
+ }
+
+ internal static IMFActivate** EnumerateDeviceSources(out uint count)
+ {
+ IMFAttributes* attributes = CreateVideoCaptureAttributes();
+ try
+ {
+ ThrowIfFailed(
+ PInvoke.MFEnumDeviceSources(attributes, out var devices, out count),
+ nameof(PInvoke.MFEnumDeviceSources));
+ return devices;
+ }
+ finally
+ {
+ Release(attributes);
+ }
+ }
+
+ internal static DeviceInfo[] EnumerateDevices()
+ {
+ Initialize();
+
+ IMFActivate** devices = null;
+ uint count = 0;
+ try
+ {
+ devices = EnumerateDeviceSources(out count);
+ var results = new List(checked((int)count));
+ for (uint index = 0; index < count; index++)
+ {
+ var activate = devices[index];
+ devices[index] = null;
+ if (activate is null)
+ {
+ continue;
+ }
+
+ try
+ {
+ var symbolicLink = GetAllocatedString(
+ activate,
+ in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim();
+ if (string.IsNullOrEmpty(symbolicLink))
+ {
+ continue;
+ }
+
+ var name = GetAllocatedString(
+ activate,
+ in PInvoke.MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME).Trim();
+ if (string.IsNullOrEmpty(name))
+ {
+ name = "Media Foundation camera";
+ }
+
+ var formats = EnumerateDeviceFormats(activate);
+ if (formats.Count != 0)
+ {
+ results.Add(new DeviceInfo(symbolicLink, name, formats));
+ }
+ }
+ catch (Exception exception)
+ {
+ TraceFailure("device inspection", exception);
+ }
+ finally
+ {
+ _ = activate->ShutdownObject();
+ Release(activate);
+ }
+ }
+ return results.ToArray();
+ }
+ finally
+ {
+ FreeActivateArray(devices, count);
+ Uninitialize();
+ }
+ }
+
+ private static IReadOnlyDictionary EnumerateDeviceFormats(
+ IMFActivate* activate)
+ {
+ IMFMediaSource* mediaSource = null;
+ IMFSourceReader* reader = null;
+ try
+ {
+ mediaSource = ActivateMediaSource(activate);
+ reader = CreateSourceReader(mediaSource);
+ var formats = new Dictionary();
+ foreach (var format in EnumerateFormats(reader))
+ {
+ if (!formats.ContainsKey(format.Characteristics))
+ {
+ formats.Add(format.Characteristics, format.Key);
+ }
+ }
+ return formats;
+ }
+ finally
+ {
+ Release(reader);
+ if (reader is null && mediaSource is not null)
+ {
+ _ = mediaSource->Shutdown();
+ }
+ Release(mediaSource);
+ }
+ }
+
+ internal static string GetAllocatedString(IMFActivate* activate, in Guid key)
+ {
+ PWSTR value = default;
+ var result = activate->GetAllocatedString(in key, out value, out _);
+ if (result.Failed || value.Value is null)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ return Marshal.PtrToStringUni((IntPtr)value.Value) ?? string.Empty;
+ }
+ finally
+ {
+ Marshal.FreeCoTaskMem((IntPtr)value.Value);
+ }
+ }
+
+ internal static IMFMediaSource* ActivateMediaSource(IMFActivate* activate)
+ {
+ ThrowIfFailed(
+ activate->ActivateObject(in IMFMediaSource.IID_Guid, out var value),
+ "IMFActivate.ActivateObject"
+ );
+ if (value is null)
+ {
+ throw new InvalidOperationException("FlashCap: Media Foundation returned no media source.");
+ }
+ return (IMFMediaSource*)value;
+ }
+
+ internal static IMFSourceReader* CreateSourceReader(IMFMediaSource* mediaSource)
+ {
+ IMFSourceReader* reader = null;
+ ThrowIfFailed(
+ PInvoke.MFCreateSourceReaderFromMediaSource(mediaSource, null, &reader),
+ nameof(PInvoke.MFCreateSourceReaderFromMediaSource));
+ if (reader is null)
+ {
+ throw new InvalidOperationException("FlashCap: Media Foundation returned no source reader.");
+ }
+ return reader;
+ }
+
+ internal static List EnumerateFormats(IMFSourceReader* reader)
+ {
+ var formats = new List();
+ for (uint index = 0; ; index++)
+ {
+ IMFMediaType* mediaType = null;
+ var result = reader->GetNativeMediaType(VideoStreamIndex, index, &mediaType);
+ if (result == HRESULT.MF_E_NO_MORE_TYPES)
+ {
+ break;
+ }
+ ThrowIfFailed(result, "IMFSourceReader.GetNativeMediaType");
+ try
+ {
+ if (mediaType is not null && TryCreateFormat(mediaType, index, out var format))
+ {
+ formats.Add(format);
+ }
+ }
+ finally
+ {
+ Release(mediaType);
+ }
+ }
+ return formats;
+ }
+
+ internal static bool TryCreateFormat(IMFMediaType* mediaType, uint index, out Format format)
+ {
+ format = default;
+ if (mediaType->GetGUID(in PInvoke.MF_MT_MAJOR_TYPE, out var majorType).Failed ||
+ majorType != PInvoke.MFMediaType_Video ||
+ mediaType->GetGUID(in PInvoke.MF_MT_SUBTYPE, out var subtype).Failed ||
+ mediaType->GetUINT64(in PInvoke.MF_MT_FRAME_SIZE, out var frameSize).Failed ||
+ mediaType->GetUINT64(in PInvoke.MF_MT_FRAME_RATE, out var frameRate).Failed)
+ {
+ return false;
+ }
+
+ var widthValue = (uint)(frameSize >> 32);
+ var heightValue = (uint)frameSize;
+ var numerator = (uint)(frameRate >> 32);
+ var denominator = (uint)frameRate;
+ if (widthValue is 0 or > int.MaxValue ||
+ heightValue is 0 or > int.MaxValue ||
+ numerator is 0 or > int.MaxValue ||
+ denominator is 0 or > int.MaxValue ||
+ !TryMapPixelFormat(subtype, out var pixelFormat, out var name))
+ {
+ return false;
+ }
+
+ var rate = new Fraction((int)numerator, (int)denominator);
+ var characteristics = new VideoCharacteristics(
+ pixelFormat, (int)widthValue, (int)heightValue, rate, name, true, name);
+ format = new Format(
+ new FormatKey(index, subtype, characteristics.Width, characteristics.Height, rate),
+ characteristics);
+ return true;
+ }
+
+ internal static bool TryMapPixelFormat(Guid subtype, out PixelFormats format, out string name)
+ {
+ if (subtype == PInvoke.MFVideoFormat_RGB24) { format = PixelFormats.RGB24; name = "RGB24"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_RGB32) { format = PixelFormats.RGB32; name = "RGB32"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_ARGB32) { format = PixelFormats.ARGB32; name = "ARGB32"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_RGB555) { format = PixelFormats.RGB15; name = "RGB555"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_RGB565) { format = PixelFormats.RGB16; name = "RGB565"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_MJPG) { format = PixelFormats.JPEG; name = "MJPG"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_UYVY) { format = PixelFormats.UYVY; name = "UYVY"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_YUY2) { format = PixelFormats.YUYV; name = "YUY2"; return true; }
+ if (subtype == PInvoke.MFVideoFormat_NV12) { format = PixelFormats.NV12; name = "NV12"; return true; }
+ format = PixelFormats.Unknown;
+ name = subtype.ToString("D");
+ return false;
+ }
+
+ internal static FrameLayout GetFrameLayout(
+ PixelFormats format,
+ int width,
+ int height,
+ int? defaultStride,
+ int bufferLength)
+ {
+ var rowLength = format switch
+ {
+ PixelFormats.RGB24 => checked(width * 3),
+ PixelFormats.RGB32 or PixelFormats.ARGB32 => checked(width * 4),
+ PixelFormats.RGB15 or PixelFormats.RGB16 or PixelFormats.UYVY or PixelFormats.YUYV =>
+ checked(width * 2),
+ PixelFormats.NV12 => width,
+ _ => throw new ArgumentOutOfRangeException(nameof(format)),
+ };
+ var rows = checked(height + (format == PixelFormats.NV12 ? (height + 1) / 2 : 0));
+ var rgb = format is PixelFormats.RGB15 or PixelFormats.RGB16 or
+ PixelFormats.RGB24 or PixelFormats.RGB32 or PixelFormats.ARGB32;
+ var bottomUp = defaultStride < 0 || defaultStride is null && rgb;
+ var sourceStride = defaultStride is { } stride && stride != 0 ? Math.Abs(stride) :
+ bufferLength % rows == 0 && bufferLength / rows >= rowLength ? bufferLength / rows : rowLength;
+ var targetStride = rgb ? checked((rowLength + 3) & ~3) : rowLength;
+ if (sourceStride < rowLength || (long)sourceStride * rows > bufferLength)
+ {
+ throw new ArgumentException("The frame buffer is truncated.", nameof(bufferLength));
+ }
+ return new FrameLayout(rowLength, rows, sourceStride, targetStride, bottomUp);
+ }
+
+ internal static void RepackFrame(
+ ReadOnlySpan source,
+ Span target,
+ FrameLayout layout,
+ bool reverseRows)
+ {
+ if ((long)layout.SourceStride * layout.Rows > source.Length || layout.TargetLength > target.Length)
+ {
+ throw new ArgumentException("The frame buffer is truncated.");
+ }
+
+ target.Slice(0, layout.TargetLength).Clear();
+ for (var row = 0; row < layout.Rows; row++)
+ {
+ var sourceRow = reverseRows ? layout.Rows - row - 1 : row;
+ source.Slice(sourceRow * layout.SourceStride, layout.RowLength).
+ CopyTo(target.Slice(row * layout.TargetStride, layout.RowLength));
+ }
+ }
+
+
+
+ internal static void FreeActivateArray(IMFActivate** devices, uint count)
+ {
+ if (devices is null)
+ {
+ return;
+ }
+ for (uint index = 0; index < count; index++)
+ {
+ Release(devices[index]);
+ }
+ Marshal.FreeCoTaskMem((IntPtr)devices);
+ }
+
+ internal static void Release(T* value) where T : unmanaged
+ {
+ if (value is not null)
+ {
+ _ = ((IUnknown*)value)->Release();
+ }
+ }
+}
+#endif
diff --git a/FlashCap.Core/Internal/NativeMethods.cs b/FlashCap.Core/Internal/NativeMethods.cs
index dd65909..f9ce3d1 100644
--- a/FlashCap.Core/Internal/NativeMethods.cs
+++ b/FlashCap.Core/Internal/NativeMethods.cs
@@ -12,6 +12,7 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Security;
using System.Text;
using FlashCap.Utilities;
@@ -61,7 +62,47 @@ private static Platforms GetRuntimePlatform()
}
}
- public static readonly Platforms CurrentPlatform =
+ [SupportedOSPlatformGuard("windows")]
+ public static bool IsWindows()
+ {
+#if NET5_0_OR_GREATER
+ return OperatingSystem.IsWindows();
+#else
+ return CurrentPlatform == Platforms.Windows;
+#endif
+ }
+
+ [SupportedOSPlatformGuard("windows6.1")]
+ public static bool IsWindowsVersionAtLeast(int major, int minor = 0)
+ {
+#if NET5_0_OR_GREATER
+ return OperatingSystem.IsWindowsVersionAtLeast(major, minor);
+#else
+ return CurrentPlatform == Platforms.Windows; // TODO: Find out windows revision
+#endif
+ }
+
+ [SupportedOSPlatformGuard("linux")]
+ public static bool IsLinux()
+ {
+#if NET5_0_OR_GREATER
+ return OperatingSystem.IsLinux();
+#else
+ return CurrentPlatform == Platforms.Linux;
+#endif
+ }
+
+ [SupportedOSPlatformGuard("macos")]
+ public static bool IsMacOS()
+ {
+#if NET5_0_OR_GREATER
+ return OperatingSystem.IsMacOS();
+#else
+ return CurrentPlatform == Platforms.MacOS;
+#endif
+ }
+
+ private static readonly Platforms CurrentPlatform =
GetRuntimePlatform();
////////////////////////////////////////////////////////////////////////
diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs
index 2d1dc8d..4f79cbd 100644
--- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs
+++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs
@@ -10,13 +10,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Security;
using FlashCap.Utilities;
using static FlashCap.Devices.DirectShowDevice;
namespace FlashCap.Internal;
+[SupportedOSPlatform("windows")]
[SuppressUnmanagedCodeSecurity]
internal static class NativeMethods_DirectShow
{
@@ -159,6 +162,7 @@ public void Release()
}
}
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public unsafe IntPtr AllocateAndGetBih()
{
if (this.formattype == FORMAT_VideoInfo)
@@ -656,6 +660,7 @@ public static TR SafeReleaseBlock(this TIF intf, Func action)
}
}
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public static IEnumerable EnumerateDeviceMoniker(Guid deviceCategory)
{
if (CoCreateInstance(
@@ -760,6 +765,7 @@ public void Dispose()
public IntPtr pBih =>
this.pBih_;
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public unsafe AM_MEDIA_TYPE AllocateFormalMediaType()
{
// Copy.
@@ -817,6 +823,7 @@ public unsafe AM_MEDIA_TYPE AllocateFormalMediaType()
private static unsafe readonly int videoStreamConfigCapsSize =
sizeof(VIDEO_STREAM_CONFIG_CAPS);
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public static bool SetFormat(this IPin pin, VideoMediaFormat format)
{
if (pin is IAMStreamConfig streamConfig)
@@ -922,6 +929,7 @@ static unsafe bool Extract(
}
}
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public static IGraphBuilder CreateGraphBuilder()
{
if (CoCreateInstance(
@@ -940,6 +948,7 @@ public static IGraphBuilder CreateGraphBuilder()
}
}
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public static ISampleGrabber CreateSampleGrabber()
{
// OMG, the sample grabber is deplicated.
@@ -961,6 +970,7 @@ public static ISampleGrabber CreateSampleGrabber()
}
}
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public static IBaseFilter CreateNullRenderer()
{
// OMG, the null renderer is deplicated.
@@ -982,6 +992,7 @@ public static IBaseFilter CreateNullRenderer()
}
}
+ [RequiresUnreferencedCode("Direct show depends on COM runtime generation and might require unreferenced code.")]
public static ICaptureGraphBuilder2 CreateCaptureGraphBuilder()
{
if (CoCreateInstance(
diff --git a/FlashCap.Core/NativeMethods.json b/FlashCap.Core/NativeMethods.json
new file mode 100644
index 0000000..41081a0
--- /dev/null
+++ b/FlashCap.Core/NativeMethods.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "allowMarshaling": false,
+ "friendlyOverloads": {
+ "enabled": true,
+ "comOutPtrGenericOverloads": false
+ },
+ "useSafeHandles": false,
+ "comInterop": {
+ "preserveSigMethods": [ "*" ]
+ }
+}
diff --git a/FlashCap.Core/NativeMethods.txt b/FlashCap.Core/NativeMethods.txt
new file mode 100644
index 0000000..301ff23
--- /dev/null
+++ b/FlashCap.Core/NativeMethods.txt
@@ -0,0 +1,39 @@
+CoInitializeEx
+CoUninitialize
+COINIT
+IMFActivate
+IMFAttributes
+IMFMediaBuffer
+IMFMediaSource
+IMFMediaType
+IMFSample
+IMFSourceReader
+MFCreateAttributes
+MFCreateSourceReaderFromMediaSource
+MFEnumDeviceSources
+MFShutdown
+MFStartup
+MFSTARTUP_FULL
+MF_VERSION
+MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME
+MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE
+MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID
+MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK
+MF_E_NO_MORE_TYPES
+MF_MT_DEFAULT_STRIDE
+MF_MT_FRAME_RATE
+MF_MT_FRAME_SIZE
+MF_MT_MAJOR_TYPE
+MF_MT_SUBTYPE
+MF_SOURCE_READER_CONSTANTS
+MF_SOURCE_READER_FLAG
+MFMediaType_Video
+MFVideoFormat_ARGB32
+MFVideoFormat_MJPG
+MFVideoFormat_NV12
+MFVideoFormat_RGB24
+MFVideoFormat_RGB32
+MFVideoFormat_RGB555
+MFVideoFormat_RGB565
+MFVideoFormat_UYVY
+MFVideoFormat_YUY2
diff --git a/FlashCap.Core/Properties/AssemblyInfo.cs b/FlashCap.Core/Properties/AssemblyInfo.cs
index 7c45418..6ae00f4 100644
--- a/FlashCap.Core/Properties/AssemblyInfo.cs
+++ b/FlashCap.Core/Properties/AssemblyInfo.cs
@@ -11,3 +11,4 @@
[assembly: InternalsVisibleTo("FlashCap")]
[assembly: InternalsVisibleTo("FSharp.FlashCap")]
+[assembly: InternalsVisibleTo("FlashCap.Core.Tests")]