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")]