From 34a9229ab8d316b5a1cd3a7478222c7298bab1f4 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 11:41:13 +0200 Subject: [PATCH 01/18] feat: Add basic Media Foundation support --- FlashCap.Core/CaptureDeviceDescriptor.cs | 1 + FlashCap.Core/CaptureDevices.cs | 6 +- .../Devices/MediaFoundationDevice.cs | 551 ++++++++++++++++++ .../MediaFoundationDeviceDescriptor.cs | 61 ++ .../Devices/MediaFoundationDevices.cs | 149 +++++ FlashCap.Core/FlashCap.Core.csproj | 10 + .../Internal/MediaFoundationInterop.cs | 393 +++++++++++++ FlashCap.Core/NativeMethods.json | 13 + FlashCap.Core/NativeMethods.txt | 39 ++ 9 files changed, 1222 insertions(+), 1 deletion(-) create mode 100644 FlashCap.Core/Devices/MediaFoundationDevice.cs create mode 100644 FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs create mode 100644 FlashCap.Core/Devices/MediaFoundationDevices.cs create mode 100644 FlashCap.Core/Internal/MediaFoundationInterop.cs create mode 100644 FlashCap.Core/NativeMethods.json create mode 100644 FlashCap.Core/NativeMethods.txt 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..7fa1a01 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -35,7 +35,11 @@ protected virtual IEnumerable OnEnumerateDescriptors() { NativeMethods.Platforms.Windows => new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(). - Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()), + Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()) +#if NET8_0_OR_GREATER + .Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors()) +#endif + , NativeMethods.Platforms.Linux => new V4L2Devices().OnEnumerateDescriptors(), NativeMethods.Platforms.MacOS => diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs new file mode 100644 index 0000000..d5b38fe --- /dev/null +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -0,0 +1,551 @@ +#if NET8_0_OR_GREATER +//////////////////////////////////////////////////////////////////////////// +// +// 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.Threading; +using System.Threading.Tasks; +using Windows.Win32; +using Windows.Win32.Media.MediaFoundation; +using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_CONSTANTS; +using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_FLAG; + +namespace FlashCap.Devices; + +public sealed class MediaFoundationDevice : CaptureDevice +{ + private readonly object 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(default).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) + { + ObjectDisposedException.ThrowIf(this.disposed, this); + Task previousCapture; + bool alreadyRunning; + lock (this.sync) + { + previousCapture = this.captureTask; + alreadyRunning = this.IsRunning && + this.stopSource is { IsCancellationRequested: false }; + } + if (alreadyRunning) + { + return; + } + await previousCapture.WaitAsync(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 startup.Task.WaitAsync(ct).ConfigureAwait(false); + } + catch + { + await this.OnStopAsync(default).ConfigureAwait(false); + throw; + } + } + + protected override async Task OnStopAsync(CancellationToken ct) + { + this.PrepareStop(out var captureTask, out var interruptTask); + + try + { + await Task.WhenAll(interruptTask, captureTask).WaitAsync(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) + { + IMFActivate* activate = null; + IMFMediaSource* mediaSource = null; + IMFSourceReader* reader = null; + bool started = false; + bool startupCompleted = false; + try + { + if (!MediaFoundationInterop.TryInitialize(out started)) + { + throw new InvalidOperationException("FlashCap: Could not initialize Media Foundation."); + } + + activate = MediaFoundationInterop.FindActivate(this.symbolicLink); + mediaSource = MediaFoundationInterop.ActivateMediaSource(activate); + reader = MediaFoundationInterop.CreateSourceReader(mediaSource); + var defaultStride = this.ConfigureReader(reader); + stopToken.ThrowIfCancellationRequested(); + + lock (this.sync) + { + this.activeSourceReader = reader; + } + this.IsRunning = true; + startupCompleted = true; + startup.TrySetResult(); + this.ReadFrames(reader, defaultStride, stopToken); + } + catch (OperationCanceledException) when (stopToken.IsCancellationRequested) + { + if (!startupCompleted) + { + startup.TrySetCanceled(stopToken); + } + } + catch (Exception exception) + { + if (!startupCompleted) + { + startup.TrySetException(exception); + } + else + { + MediaFoundationInterop.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) + { + MediaFoundationInterop.TraceFailure("capture interruption", exception); + } + + if (reader is null && mediaSource is not null) + { + _ = mediaSource->Shutdown(); + } + MediaFoundationInterop.Release(reader); + if (activate is not null) + { + _ = activate->ShutdownObject(); + } + MediaFoundationInterop.Release(mediaSource); + MediaFoundationInterop.Release(activate); + MediaFoundationInterop.Uninitialize(started); + if (!startupCompleted) + { + startup.TrySetException(new InvalidOperationException( + "FlashCap: Media Foundation capture ended during startup.")); + } + } + } + + private unsafe int? ConfigureReader(IMFSourceReader* reader) + { + MediaFoundationInterop.ThrowIfFailed( + reader->SetStreamSelection(unchecked((uint)MF_SOURCE_READER_ALL_STREAMS), false), + "IMFSourceReader.SetStreamSelection(all)"); + MediaFoundationInterop.ThrowIfFailed( + reader->SetStreamSelection(MediaFoundationInterop.VideoStreamIndex, true), + "IMFSourceReader.SetStreamSelection(video)"); + + IMFMediaType* mediaType = null; + try + { + MediaFoundationInterop.ThrowIfFailed( + reader->GetNativeMediaType( + MediaFoundationInterop.VideoStreamIndex, + this.formatKey.MediaTypeIndex, + &mediaType), + "IMFSourceReader.GetNativeMediaType"); + if (mediaType is null || + !MediaFoundationInterop.TryCreateFormat( + mediaType, this.formatKey.MediaTypeIndex, out var selected) || + !selected.Key.Equals(this.formatKey)) + { + throw new InvalidOperationException( + "FlashCap: The selected Media Foundation format is no longer available."); + } + + MediaFoundationInterop.ThrowIfFailed( + reader->SetCurrentMediaType(MediaFoundationInterop.VideoStreamIndex, mediaType), + "IMFSourceReader.SetCurrentMediaType"); + return mediaType->GetUINT32(in PInvoke.MF_MT_DEFAULT_STRIDE, out var stride).Succeeded ? + unchecked((int)stride) : null; + } + finally + { + MediaFoundationInterop.Release(mediaType); + } + } + + private unsafe void ReadFrames(IMFSourceReader* reader, int? defaultStride, CancellationToken stopToken) + { + long? firstTimestamp = null; + long frameIndex = 0; + while (!stopToken.IsCancellationRequested) + { + uint flags = 0; + long timestamp = 0; + IMFSample* sample = null; + var result = reader->ReadSample( + MediaFoundationInterop.VideoStreamIndex, + 0, + null, + &flags, + ×tamp, + &sample); + if (result.Failed) + { + if (stopToken.IsCancellationRequested) + { + break; + } + MediaFoundationInterop.ThrowIfFailed(result, "IMFSourceReader.ReadSample"); + } + + try + { + if (stopToken.IsCancellationRequested) + { + break; + } + var readerFlags = (MF_SOURCE_READER_FLAG)flags; + if ((readerFlags & (MF_SOURCE_READERF_ERROR | + MF_SOURCE_READERF_ENDOFSTREAM | + MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED | + MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)) != 0) + { + throw new InvalidOperationException( + $"FlashCap: Media Foundation capture stream changed state ({readerFlags})."); + } + if (sample is null || (readerFlags & MF_SOURCE_READERF_STREAMTICK) != 0) + { + continue; + } + + firstTimestamp ??= timestamp; + this.ProcessSample( + sample, + defaultStride, + Math.Max(0, timestamp - firstTimestamp.Value) / 10, + frameIndex++); + } + finally + { + MediaFoundationInterop.Release(sample); + } + } + } + + private unsafe void ProcessSample(IMFSample* sample, int? defaultStride, long timestamp, long frameIndex) + { + IMFMediaBuffer* buffer = null; + MediaFoundationInterop.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; + MediaFoundationInterop.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."); + } + + var frame = this.NormalizeFrame(data, checked((int)currentLength), defaultStride); + try + { + if (frame.Pointer != IntPtr.Zero) + { + this.frameProcessor!.OnFrameArrived( + this, frame.Pointer, frame.Length, timestamp, frameIndex); + } + else + { + fixed (byte* repacked = this.repackBuffer!) + { + this.frameProcessor!.OnFrameArrived( + this, (IntPtr)repacked, frame.Length, timestamp, frameIndex); + } + } + } + catch (Exception exception) + { + MediaFoundationInterop.TraceFailure("frame callback", exception); + } + } + finally + { + if (locked) + { + _ = buffer->Unlock(); + } + MediaFoundationInterop.Release(buffer); + } + } + + 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) + { + if (!MediaFoundationInterop.TryInitialize(out var started)) + { + return; + } + try + { + if (reader is not null) + { + MediaFoundationInterop.ThrowIfFailed( + reader->Flush(unchecked((uint)MF_SOURCE_READER_ALL_STREAMS)), + "IMFSourceReader.Flush"); + } + } + finally + { + MediaFoundationInterop.Uninitialize(started); + } + } + + protected override unsafe void OnCapture( + IntPtr pData, + int size, + long timestampMicroseconds, + long frameIndex, + PixelBuffer buffer) + { + buffer.CopyIn( + this.bitmapHeader, + pData, + size, + timestampMicroseconds, + frameIndex, + this.transcodeFormat); + } + + private readonly struct FrameMemory + { + internal FrameMemory(IntPtr pointer, int length) + { + this.Pointer = pointer; + this.Length = length; + } + + internal IntPtr Pointer { get; } + internal int Length { get; } + } +} +#endif diff --git a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs new file mode 100644 index 0000000..e4b23ab --- /dev/null +++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs @@ -0,0 +1,61 @@ +#if NET8_0_OR_GREATER +//////////////////////////////////////////////////////////////////////////// +// +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace FlashCap.Devices; + +public sealed class MediaFoundationDeviceDescriptor : CaptureDeviceDescriptor +{ + private readonly string symbolicLink; + private readonly MediaFoundationInterop.Format[] formats; + + internal MediaFoundationDeviceDescriptor( + string symbolicLink, + string name, + string description, + MediaFoundationInterop.Format[] formats, + BufferPool defaultBufferPool) : + base(name, description, formats.Select(format => format.Characteristics).ToArray(), defaultBufferPool) + { + this.symbolicLink = symbolicLink; + this.formats = formats; + } + + public override object Identity => this.symbolicLink; + + public override DeviceTypes DeviceType => DeviceTypes.MediaFoundation; + + protected override Task OnOpenWithFrameProcessorAsync( + VideoCharacteristics characteristics, + TranscodeFormats transcodeFormat, + FrameProcessor frameProcessor, + CancellationToken ct) + { + var format = this.formats.FirstOrDefault(candidate => candidate.Characteristics.Equals(characteristics)); + if (format.Characteristics is null) + { + throw new System.ArgumentException( + "FlashCap: The selected Media Foundation format is not available.", + nameof(characteristics)); + } + + return this.InternalOnOpenWithFrameProcessorAsync( + new MediaFoundationDevice(this.symbolicLink, this.Name, format.Key), + 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..322b6f7 --- /dev/null +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -0,0 +1,149 @@ +#if NET8_0_OR_GREATER +//////////////////////////////////////////////////////////////////////////// +// +// 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.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Windows.Win32; +using Windows.Win32.Media.MediaFoundation; + +namespace FlashCap.Devices; + +public sealed class MediaFoundationDevices : CaptureDevices +{ + public MediaFoundationDevices() : + this(new DefaultBufferPool()) + { + } + + public MediaFoundationDevices(BufferPool defaultBufferPool) : + base(defaultBufferPool) + { + } + + protected override IEnumerable OnEnumerateDescriptors() + { + if (!OperatingSystem.IsWindows()) + { + return Array.Empty(); + } + + try + { + return Task.Factory.StartNew( + this.EnumerateDescriptors, + CancellationToken.None, + TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default).GetAwaiter().GetResult(); + } + catch (Exception exception) + { + MediaFoundationInterop.TraceFailure("device discovery", exception); + return Array.Empty(); + } + } + + private unsafe CaptureDeviceDescriptor[] EnumerateDescriptors() + { + if (!MediaFoundationInterop.TryInitialize(out var started)) + { + return Array.Empty(); + } + + IMFActivate** devices = null; + uint count = 0; + try + { + devices = MediaFoundationInterop.EnumerateDeviceSources(out count); + var descriptors = 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 = MediaFoundationInterop.GetAllocatedString( + activate, + in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim(); + if (string.IsNullOrEmpty(symbolicLink)) + { + continue; + } + + var name = MediaFoundationInterop.GetAllocatedString( + activate, + in PInvoke.MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME).Trim(); + if (string.IsNullOrEmpty(name)) + { + name = "Media Foundation camera"; + } + + var formats = EnumerateFormats(activate); + if (formats.Length == 0) + { + continue; + } + + descriptors.Add(new MediaFoundationDeviceDescriptor( + symbolicLink, + name, + $"{name} (Media Foundation)", + formats, + this.DefaultBufferPool)); + } + catch (Exception exception) + { + MediaFoundationInterop.TraceFailure("device inspection", exception); + } + finally + { + _ = activate->ShutdownObject(); + MediaFoundationInterop.Release(activate); + } + } + return descriptors.ToArray(); + } + finally + { + MediaFoundationInterop.FreeActivateArray(devices, count); + MediaFoundationInterop.Uninitialize(started); + } + } + + private static unsafe MediaFoundationInterop.Format[] EnumerateFormats(IMFActivate* activate) + { + IMFMediaSource* mediaSource = null; + IMFSourceReader* reader = null; + try + { + mediaSource = MediaFoundationInterop.ActivateMediaSource(activate); + reader = MediaFoundationInterop.CreateSourceReader(mediaSource); + return MediaFoundationInterop.EnumerateFormats(reader).ToArray(); + } + finally + { + MediaFoundationInterop.Release(reader); + if (reader is null && mediaSource is not null) + { + _ = mediaSource->Shutdown(); + } + MediaFoundationInterop.Release(mediaSource); + } + } +} +#endif diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj index c59cbb8..99d5251 100644 --- a/FlashCap.Core/FlashCap.Core.csproj +++ b/FlashCap.Core/FlashCap.Core.csproj @@ -7,10 +7,20 @@ true + + true + + + + + runtime; build; native; contentfiles; analyzers + + + diff --git a/FlashCap.Core/Internal/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundationInterop.cs new file mode 100644 index 0000000..e6002a2 --- /dev/null +++ b/FlashCap.Core/Internal/MediaFoundationInterop.cs @@ -0,0 +1,393 @@ +#if NET8_0_OR_GREATER +//////////////////////////////////////////////////////////////////////////// +// +// 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.Utilities; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Media.MediaFoundation; +using Windows.Win32.System.Com; + +namespace FlashCap.Internal; + +internal static unsafe class MediaFoundationInterop +{ + internal const uint VideoStreamIndex = 0; + + internal readonly struct FormatKey : IEquatable + { + internal FormatKey(uint mediaTypeIndex, Guid subtype, int width, int height, Fraction frameRate) + { + this.MediaTypeIndex = mediaTypeIndex; + this.Subtype = subtype; + this.Width = width; + this.Height = height; + this.FrameRate = frameRate; + } + + internal uint MediaTypeIndex { get; } + internal Guid Subtype { get; } + internal int Width { get; } + internal int Height { get; } + internal Fraction FrameRate { get; } + + public bool Equals(FormatKey other) => + this.MediaTypeIndex == other.MediaTypeIndex && + this.Subtype == other.Subtype && + this.Width == other.Width && + this.Height == other.Height && + this.FrameRate == other.FrameRate; + + public override bool Equals(object? obj) => obj is FormatKey other && this.Equals(other); + + public override int GetHashCode() => + HashCode.Combine(this.MediaTypeIndex, this.Subtype, this.Width, this.Height, this.FrameRate); + } + + internal readonly struct Format + { + internal Format(FormatKey key, VideoCharacteristics characteristics) + { + this.Key = key; + this.Characteristics = characteristics; + } + + internal FormatKey Key { get; } + internal VideoCharacteristics Characteristics { get; } + } + + internal readonly struct FrameLayout + { + internal FrameLayout( + int rowLength, + int rows, + int sourceStride, + int targetStride, + bool bottomUp) + { + this.RowLength = rowLength; + this.Rows = rows; + this.SourceStride = sourceStride; + this.TargetStride = targetStride; + this.BottomUp = bottomUp; + } + + internal int RowLength { get; } + internal int Rows { get; } + internal int SourceStride { get; } + internal int TargetStride { get; } + internal bool BottomUp { get; } + internal int TargetLength => checked(this.TargetStride * this.Rows); + } + + 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 bool TryInitialize(out bool mediaFoundationStarted) + { + mediaFoundationStarted = false; + var result = PInvoke.CoInitializeEx(COINIT.COINIT_MULTITHREADED); + if (result.Failed) + { + Trace.WriteLine( + $"FlashCap: CoInitializeEx failed (HRESULT=0x{unchecked((uint)result.Value):X8})."); + return false; + } + + result = PInvoke.MFStartup(PInvoke.MF_VERSION, PInvoke.MFSTARTUP_FULL); + if (result.Failed) + { + Trace.WriteLine( + $"FlashCap: MFStartup failed (HRESULT=0x{unchecked((uint)result.Value):X8})."); + PInvoke.CoUninitialize(); + return false; + } + + mediaFoundationStarted = true; + return true; + } + + internal static void Uninitialize(bool mediaFoundationStarted) + { + if (mediaFoundationStarted) + { + _ = 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 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) + { + void* value = null; + ThrowIfFailed( + activate->ActivateObject(in IMFMediaSource.IID_Guid, out 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[..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 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."); + } + + 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) + { + _ = ((Windows.Win32.System.Com.IUnknown*)value)->Release(); + } + } + + internal static void TraceFailure(string operation, Exception exception) => + Trace.WriteLine($"FlashCap: Media Foundation {operation} failed: {exception}"); +} +#endif diff --git a/FlashCap.Core/NativeMethods.json b/FlashCap.Core/NativeMethods.json new file mode 100644 index 0000000..b85ec87 --- /dev/null +++ b/FlashCap.Core/NativeMethods.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "allowMarshaling": false, + "friendlyOverloads": { + "enabled": true, + "includePointerOverloads": 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 From a7c6b99436997cbfe5732b2d8d81dbf4279631a2 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 11:54:34 +0200 Subject: [PATCH 02/18] refactor: Explicit dictionary format lookup --- .../Devices/MediaFoundationDeviceDescriptor.cs | 14 +++++++------- FlashCap.Core/Devices/MediaFoundationDevices.cs | 16 +++++++--------- FlashCap.Core/Internal/MediaFoundationInterop.cs | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs index e4b23ab..91f807d 100644 --- a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs @@ -9,6 +9,7 @@ //////////////////////////////////////////////////////////////////////////// using FlashCap.Internal; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -18,18 +19,18 @@ namespace FlashCap.Devices; public sealed class MediaFoundationDeviceDescriptor : CaptureDeviceDescriptor { private readonly string symbolicLink; - private readonly MediaFoundationInterop.Format[] formats; + private readonly IReadOnlyDictionary characteristicToFormatLookup; internal MediaFoundationDeviceDescriptor( string symbolicLink, string name, string description, - MediaFoundationInterop.Format[] formats, + IReadOnlyDictionary characteristicToFormatLookup, BufferPool defaultBufferPool) : - base(name, description, formats.Select(format => format.Characteristics).ToArray(), defaultBufferPool) + base(name, description, characteristicToFormatLookup.Keys.ToArray(), defaultBufferPool) { this.symbolicLink = symbolicLink; - this.formats = formats; + this.characteristicToFormatLookup = characteristicToFormatLookup; } public override object Identity => this.symbolicLink; @@ -42,8 +43,7 @@ protected override Task OnOpenWithFrameProcessorAsync( FrameProcessor frameProcessor, CancellationToken ct) { - var format = this.formats.FirstOrDefault(candidate => candidate.Characteristics.Equals(characteristics)); - if (format.Characteristics is null) + if (!this.characteristicToFormatLookup.TryGetValue(characteristics, out var formatKey)) { throw new System.ArgumentException( "FlashCap: The selected Media Foundation format is not available.", @@ -51,7 +51,7 @@ protected override Task OnOpenWithFrameProcessorAsync( } return this.InternalOnOpenWithFrameProcessorAsync( - new MediaFoundationDevice(this.symbolicLink, this.Name, format.Key), + new MediaFoundationDevice(this.symbolicLink, this.Name, formatKey), characteristics, transcodeFormat, frameProcessor, diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index 322b6f7..9cde71d 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -19,18 +19,13 @@ namespace FlashCap.Devices; -public sealed class MediaFoundationDevices : CaptureDevices +public sealed class MediaFoundationDevices(BufferPool defaultBufferPool) : CaptureDevices(defaultBufferPool) { public MediaFoundationDevices() : this(new DefaultBufferPool()) { } - public MediaFoundationDevices(BufferPool defaultBufferPool) : - base(defaultBufferPool) - { - } - protected override IEnumerable OnEnumerateDescriptors() { if (!OperatingSystem.IsWindows()) @@ -94,7 +89,7 @@ private unsafe CaptureDeviceDescriptor[] EnumerateDescriptors() } var formats = EnumerateFormats(activate); - if (formats.Length == 0) + if (formats.Count == 0) { continue; } @@ -125,7 +120,8 @@ private unsafe CaptureDeviceDescriptor[] EnumerateDescriptors() } } - private static unsafe MediaFoundationInterop.Format[] EnumerateFormats(IMFActivate* activate) + private static unsafe Dictionary EnumerateFormats( + IMFActivate* activate) { IMFMediaSource* mediaSource = null; IMFSourceReader* reader = null; @@ -133,7 +129,9 @@ private static unsafe MediaFoundationInterop.Format[] EnumerateFormats(IMFActiva { mediaSource = MediaFoundationInterop.ActivateMediaSource(activate); reader = MediaFoundationInterop.CreateSourceReader(mediaSource); - return MediaFoundationInterop.EnumerateFormats(reader).ToArray(); + return MediaFoundationInterop.EnumerateFormats(reader) + .DistinctBy(format => format.Characteristics) + .ToDictionary(format => format.Characteristics, format => format.Key); } finally { diff --git a/FlashCap.Core/Internal/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundationInterop.cs index e6002a2..1e5308c 100644 --- a/FlashCap.Core/Internal/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundationInterop.cs @@ -383,7 +383,7 @@ internal static void Release(T* value) where T : unmanaged { if (value is not null) { - _ = ((Windows.Win32.System.Com.IUnknown*)value)->Release(); + _ = ((IUnknown*)value)->Release(); } } From ff3af07ec9337d9cb892239a44bb43c9aac3404c Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 13:48:00 +0200 Subject: [PATCH 03/18] refactor: Small cleanup and move into records --- .../Devices/MediaFoundationDevice.cs | 23 +-- .../MediaFoundationDeviceDescriptor.cs | 2 + .../Devices/MediaFoundationDevices.cs | 112 ++--------- .../Internal/MediaFoundationInterop.cs | 182 +++++++++++------- 4 files changed, 140 insertions(+), 179 deletions(-) diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index d5b38fe..e9d2574 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -12,18 +12,23 @@ using System; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using Windows.Win32; using Windows.Win32.Media.MediaFoundation; using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_CONSTANTS; using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_FLAG; +#if !NET9_0_OR_GREATER +using Lock = object; +#endif namespace FlashCap.Devices; +[SupportedOSPlatform("windows6.0")] public sealed class MediaFoundationDevice : CaptureDevice { - private readonly object sync = new(); + private readonly Lock sync = new(); private readonly string symbolicLink; private readonly MediaFoundationInterop.FormatKey formatKey; @@ -92,7 +97,7 @@ protected override async Task OnDisposeAsync() Exception? stopFailure = null; try { - await this.OnStopAsync(default).ConfigureAwait(false); + await this.OnStopAsync(CancellationToken.None).ConfigureAwait(false); } catch (Exception exception) { @@ -161,7 +166,7 @@ protected override async Task OnStartAsync(CancellationToken ct) } catch { - await this.OnStopAsync(default).ConfigureAwait(false); + await this.OnStopAsync(CancellationToken.None).ConfigureAwait(false); throw; } } @@ -536,16 +541,6 @@ protected override unsafe void OnCapture( this.transcodeFormat); } - private readonly struct FrameMemory - { - internal FrameMemory(IntPtr pointer, int length) - { - this.Pointer = pointer; - this.Length = length; - } - - internal IntPtr Pointer { get; } - internal int Length { get; } - } + private readonly record struct FrameMemory(IntPtr Pointer, int Length); } #endif diff --git a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs index 91f807d..ee79c3b 100644 --- a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs @@ -11,11 +11,13 @@ using FlashCap.Internal; using System.Collections.Generic; using System.Linq; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; namespace FlashCap.Devices; +[SupportedOSPlatform("windows6.0")] public sealed class MediaFoundationDeviceDescriptor : CaptureDeviceDescriptor { private readonly string symbolicLink; diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index 9cde71d..a975e36 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -12,13 +12,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; -using Windows.Win32; -using Windows.Win32.Media.MediaFoundation; namespace FlashCap.Devices; +[SupportedOSPlatform("windows6.0")] public sealed class MediaFoundationDevices(BufferPool defaultBufferPool) : CaptureDevices(defaultBufferPool) { public MediaFoundationDevices() : @@ -30,13 +30,20 @@ protected override IEnumerable OnEnumerateDescriptors() { if (!OperatingSystem.IsWindows()) { - return Array.Empty(); + return []; } try { return Task.Factory.StartNew( - this.EnumerateDescriptors, + () => 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(); @@ -44,104 +51,9 @@ protected override IEnumerable OnEnumerateDescriptors() catch (Exception exception) { MediaFoundationInterop.TraceFailure("device discovery", exception); - return Array.Empty(); + return []; } } - private unsafe CaptureDeviceDescriptor[] EnumerateDescriptors() - { - if (!MediaFoundationInterop.TryInitialize(out var started)) - { - return Array.Empty(); - } - - IMFActivate** devices = null; - uint count = 0; - try - { - devices = MediaFoundationInterop.EnumerateDeviceSources(out count); - var descriptors = 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 = MediaFoundationInterop.GetAllocatedString( - activate, - in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim(); - if (string.IsNullOrEmpty(symbolicLink)) - { - continue; - } - - var name = MediaFoundationInterop.GetAllocatedString( - activate, - in PInvoke.MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME).Trim(); - if (string.IsNullOrEmpty(name)) - { - name = "Media Foundation camera"; - } - - var formats = EnumerateFormats(activate); - if (formats.Count == 0) - { - continue; - } - - descriptors.Add(new MediaFoundationDeviceDescriptor( - symbolicLink, - name, - $"{name} (Media Foundation)", - formats, - this.DefaultBufferPool)); - } - catch (Exception exception) - { - MediaFoundationInterop.TraceFailure("device inspection", exception); - } - finally - { - _ = activate->ShutdownObject(); - MediaFoundationInterop.Release(activate); - } - } - return descriptors.ToArray(); - } - finally - { - MediaFoundationInterop.FreeActivateArray(devices, count); - MediaFoundationInterop.Uninitialize(started); - } - } - - private static unsafe Dictionary EnumerateFormats( - IMFActivate* activate) - { - IMFMediaSource* mediaSource = null; - IMFSourceReader* reader = null; - try - { - mediaSource = MediaFoundationInterop.ActivateMediaSource(activate); - reader = MediaFoundationInterop.CreateSourceReader(mediaSource); - return MediaFoundationInterop.EnumerateFormats(reader) - .DistinctBy(format => format.Characteristics) - .ToDictionary(format => format.Characteristics, format => format.Key); - } - finally - { - MediaFoundationInterop.Release(reader); - if (reader is null && mediaSource is not null) - { - _ = mediaSource->Shutdown(); - } - MediaFoundationInterop.Release(mediaSource); - } - } } #endif diff --git a/FlashCap.Core/Internal/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundationInterop.cs index 1e5308c..00f2c44 100644 --- a/FlashCap.Core/Internal/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundationInterop.cs @@ -12,7 +12,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Media.MediaFoundation; @@ -20,73 +22,34 @@ namespace FlashCap.Internal; +[SupportedOSPlatform("windows6.0")] internal static unsafe class MediaFoundationInterop { internal const uint VideoStreamIndex = 0; - internal readonly struct FormatKey : IEquatable + 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 FormatKey(uint mediaTypeIndex, Guid subtype, int width, int height, Fraction frameRate) - { - this.MediaTypeIndex = mediaTypeIndex; - this.Subtype = subtype; - this.Width = width; - this.Height = height; - this.FrameRate = frameRate; - } - - internal uint MediaTypeIndex { get; } - internal Guid Subtype { get; } - internal int Width { get; } - internal int Height { get; } - internal Fraction FrameRate { get; } - - public bool Equals(FormatKey other) => - this.MediaTypeIndex == other.MediaTypeIndex && - this.Subtype == other.Subtype && - this.Width == other.Width && - this.Height == other.Height && - this.FrameRate == other.FrameRate; - - public override bool Equals(object? obj) => obj is FormatKey other && this.Equals(other); - - public override int GetHashCode() => - HashCode.Combine(this.MediaTypeIndex, this.Subtype, this.Width, this.Height, this.FrameRate); - } - - internal readonly struct Format - { - internal Format(FormatKey key, VideoCharacteristics characteristics) - { - this.Key = key; - this.Characteristics = characteristics; - } - - internal FormatKey Key { get; } - internal VideoCharacteristics Characteristics { get; } - } - - internal readonly struct FrameLayout - { - internal FrameLayout( - int rowLength, - int rows, - int sourceStride, - int targetStride, - bool bottomUp) - { - this.RowLength = rowLength; - this.Rows = rows; - this.SourceStride = sourceStride; - this.TargetStride = targetStride; - this.BottomUp = bottomUp; - } - - internal int RowLength { get; } - internal int Rows { get; } - internal int SourceStride { get; } - internal int TargetStride { get; } - internal bool BottomUp { get; } internal int TargetLength => checked(this.TargetStride * this.Rows); } @@ -168,6 +131,95 @@ internal static void Uninitialize(bool mediaFoundationStarted) } } + internal static DeviceInfo[] EnumerateDevices() + { + if (!TryInitialize(out var started)) + { + return []; + } + + 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(started); + } + } + + private static IReadOnlyDictionary EnumerateDeviceFormats( + IMFActivate* activate) + { + IMFMediaSource* mediaSource = null; + IMFSourceReader* reader = null; + try + { + mediaSource = ActivateMediaSource(activate); + reader = CreateSourceReader(mediaSource); + return EnumerateFormats(reader) + .DistinctBy(format => format.Characteristics) + .ToDictionary(format => format.Characteristics, format => format.Key); + } + 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; @@ -189,10 +241,10 @@ internal static string GetAllocatedString(IMFActivate* activate, in Guid key) internal static IMFMediaSource* ActivateMediaSource(IMFActivate* activate) { - void* value = null; ThrowIfFailed( - activate->ActivateObject(in IMFMediaSource.IID_Guid, out value), - "IMFActivate.ActivateObject"); + 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."); From 81ba7b4cb56e44cce8a8bb518c0a20fad3f740fb Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 14:54:06 +0200 Subject: [PATCH 04/18] refactor: Simplify Initialize --- FlashCap.Core/CaptureDevices.cs | 7 +--- .../Devices/MediaFoundationDevice.cs | 20 +++++----- .../Devices/MediaFoundationDevices.cs | 4 +- .../Internal/MediaFoundationInterop.cs | 37 ++++++------------- 4 files changed, 26 insertions(+), 42 deletions(-) diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index 7fa1a01..7f4dd82 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -18,17 +18,14 @@ namespace FlashCap; -public class CaptureDevices +public class CaptureDevices(BufferPool defaultBufferPool) { - protected readonly BufferPool DefaultBufferPool; + protected readonly BufferPool DefaultBufferPool = defaultBufferPool; public CaptureDevices() : this(new DefaultBufferPool()) { } - - public CaptureDevices(BufferPool defaultBufferPool) => - this.DefaultBufferPool = defaultBufferPool; protected virtual IEnumerable OnEnumerateDescriptors() => NativeMethods.CurrentPlatform switch diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index e9d2574..188293d 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -222,14 +222,12 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stop IMFActivate* activate = null; IMFMediaSource* mediaSource = null; IMFSourceReader* reader = null; - bool started = false; + bool initialized = false; bool startupCompleted = false; try { - if (!MediaFoundationInterop.TryInitialize(out started)) - { - throw new InvalidOperationException("FlashCap: Could not initialize Media Foundation."); - } + MediaFoundationInterop.Initialize(); + initialized = true; activate = MediaFoundationInterop.FindActivate(this.symbolicLink); mediaSource = MediaFoundationInterop.ActivateMediaSource(activate); @@ -293,7 +291,10 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stop } MediaFoundationInterop.Release(mediaSource); MediaFoundationInterop.Release(activate); - MediaFoundationInterop.Uninitialize(started); + if (initialized) + { + MediaFoundationInterop.Uninitialize(); + } if (!startupCompleted) { startup.TrySetException(new InvalidOperationException( @@ -506,10 +507,7 @@ format is PixelFormats.RGB15 or PixelFormats.RGB16 or private static unsafe void Interrupt(IMFSourceReader* reader) { - if (!MediaFoundationInterop.TryInitialize(out var started)) - { - return; - } + MediaFoundationInterop.Initialize(); try { if (reader is not null) @@ -521,7 +519,7 @@ private static unsafe void Interrupt(IMFSourceReader* reader) } finally { - MediaFoundationInterop.Uninitialize(started); + MediaFoundationInterop.Uninitialize(); } } diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index a975e36..98bbefa 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -11,6 +11,7 @@ using FlashCap.Internal; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.Versioning; using System.Threading; @@ -30,7 +31,8 @@ protected override IEnumerable OnEnumerateDescriptors() { if (!OperatingSystem.IsWindows()) { - return []; + // The class is guarded by SupportedOSPlatform + throw new UnreachableException(); } try diff --git a/FlashCap.Core/Internal/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundationInterop.cs index 00f2c44..a3fdc07 100644 --- a/FlashCap.Core/Internal/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundationInterop.cs @@ -62,37 +62,27 @@ internal static void ThrowIfFailed(HRESULT result, string operation) } } - internal static bool TryInitialize(out bool mediaFoundationStarted) + /// + /// 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() { - mediaFoundationStarted = false; var result = PInvoke.CoInitializeEx(COINIT.COINIT_MULTITHREADED); - if (result.Failed) - { - Trace.WriteLine( - $"FlashCap: CoInitializeEx failed (HRESULT=0x{unchecked((uint)result.Value):X8})."); - return false; - } + ThrowIfFailed(result, nameof(PInvoke.CoInitializeEx)); result = PInvoke.MFStartup(PInvoke.MF_VERSION, PInvoke.MFSTARTUP_FULL); if (result.Failed) { - Trace.WriteLine( - $"FlashCap: MFStartup failed (HRESULT=0x{unchecked((uint)result.Value):X8})."); PInvoke.CoUninitialize(); - return false; + ThrowIfFailed(result, nameof(PInvoke.MFStartup)); } - - mediaFoundationStarted = true; - return true; } - internal static void Uninitialize(bool mediaFoundationStarted) + internal static void Uninitialize() { - if (mediaFoundationStarted) - { - _ = PInvoke.MFShutdown(); - PInvoke.CoUninitialize(); - } + _ = PInvoke.MFShutdown(); + PInvoke.CoUninitialize(); } internal static IMFAttributes* CreateVideoCaptureAttributes() @@ -133,10 +123,7 @@ internal static void Uninitialize(bool mediaFoundationStarted) internal static DeviceInfo[] EnumerateDevices() { - if (!TryInitialize(out var started)) - { - return []; - } + Initialize(); IMFActivate** devices = null; uint count = 0; @@ -192,7 +179,7 @@ internal static DeviceInfo[] EnumerateDevices() finally { FreeActivateArray(devices, count); - Uninitialize(started); + Uninitialize(); } } From b33f2a266577f011f363cecb9873c5463c7e245d Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 15:29:32 +0200 Subject: [PATCH 05/18] refactor: Move mediafoundation code into their own, separate files --- .../Devices/MediaFoundationDevice.cs | 203 +++------------ .../MediaFoundationDeviceDescriptor.cs | 1 + .../Devices/MediaFoundationDevices.cs | 3 +- .../MediaFoundation/CaptureSession.cs | 234 ++++++++++++++++++ .../MediaFoundation/MediaFoundationHelpers.cs | 31 +++ .../MediaFoundationInterop.cs | 48 +--- 6 files changed, 307 insertions(+), 213 deletions(-) create mode 100644 FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs create mode 100644 FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs rename FlashCap.Core/Internal/{ => MediaFoundation}/MediaFoundationInterop.cs (90%) diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index 188293d..25d6e74 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -10,15 +10,15 @@ using FlashCap.Internal; using System; +using System.Diagnostics; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; -using Windows.Win32; using Windows.Win32.Media.MediaFoundation; +using FlashCap.Internal.MediaFoundation; using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_CONSTANTS; -using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_FLAG; #if !NET9_0_OR_GREATER using Lock = object; #endif @@ -219,9 +219,7 @@ private unsafe void PrepareStop(out Task captureTask, out Task interruptTask) private unsafe void Capture(TaskCompletionSource startup, CancellationToken stopToken) { - IMFActivate* activate = null; - IMFMediaSource* mediaSource = null; - IMFSourceReader* reader = null; + CaptureSession? session = null; bool initialized = false; bool startupCompleted = false; try @@ -229,20 +227,17 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stop MediaFoundationInterop.Initialize(); initialized = true; - activate = MediaFoundationInterop.FindActivate(this.symbolicLink); - mediaSource = MediaFoundationInterop.ActivateMediaSource(activate); - reader = MediaFoundationInterop.CreateSourceReader(mediaSource); - var defaultStride = this.ConfigureReader(reader); + session = CaptureSession.Open(this.symbolicLink, this.formatKey); stopToken.ThrowIfCancellationRequested(); lock (this.sync) { - this.activeSourceReader = reader; + this.activeSourceReader = session.SourceReader; } this.IsRunning = true; startupCompleted = true; startup.TrySetResult(); - this.ReadFrames(reader, defaultStride, stopToken); + session.ReadFrames(stopToken, this.OnFrame); } catch (OperationCanceledException) when (stopToken.IsCancellationRequested) { @@ -259,7 +254,7 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stop } else { - MediaFoundationInterop.TraceFailure("capture", exception); + MediaFoundationHelpers.TraceFailure("capture", exception); } } finally @@ -277,20 +272,10 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stop } catch (Exception exception) { - MediaFoundationInterop.TraceFailure("capture interruption", exception); + MediaFoundationHelpers.TraceFailure("capture interruption", exception); } - if (reader is null && mediaSource is not null) - { - _ = mediaSource->Shutdown(); - } - MediaFoundationInterop.Release(reader); - if (activate is not null) - { - _ = activate->ShutdownObject(); - } - MediaFoundationInterop.Release(mediaSource); - MediaFoundationInterop.Release(activate); + session?.Dispose(); if (initialized) { MediaFoundationInterop.Uninitialize(); @@ -303,158 +288,34 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stop } } - private unsafe int? ConfigureReader(IMFSourceReader* reader) - { - MediaFoundationInterop.ThrowIfFailed( - reader->SetStreamSelection(unchecked((uint)MF_SOURCE_READER_ALL_STREAMS), false), - "IMFSourceReader.SetStreamSelection(all)"); - MediaFoundationInterop.ThrowIfFailed( - reader->SetStreamSelection(MediaFoundationInterop.VideoStreamIndex, true), - "IMFSourceReader.SetStreamSelection(video)"); - - IMFMediaType* mediaType = null; - try - { - MediaFoundationInterop.ThrowIfFailed( - reader->GetNativeMediaType( - MediaFoundationInterop.VideoStreamIndex, - this.formatKey.MediaTypeIndex, - &mediaType), - "IMFSourceReader.GetNativeMediaType"); - if (mediaType is null || - !MediaFoundationInterop.TryCreateFormat( - mediaType, this.formatKey.MediaTypeIndex, out var selected) || - !selected.Key.Equals(this.formatKey)) - { - throw new InvalidOperationException( - "FlashCap: The selected Media Foundation format is no longer available."); - } - - MediaFoundationInterop.ThrowIfFailed( - reader->SetCurrentMediaType(MediaFoundationInterop.VideoStreamIndex, mediaType), - "IMFSourceReader.SetCurrentMediaType"); - return mediaType->GetUINT32(in PInvoke.MF_MT_DEFAULT_STRIDE, out var stride).Succeeded ? - unchecked((int)stride) : null; - } - finally - { - MediaFoundationInterop.Release(mediaType); - } - } - - private unsafe void ReadFrames(IMFSourceReader* reader, int? defaultStride, CancellationToken stopToken) - { - long? firstTimestamp = null; - long frameIndex = 0; - while (!stopToken.IsCancellationRequested) - { - uint flags = 0; - long timestamp = 0; - IMFSample* sample = null; - var result = reader->ReadSample( - MediaFoundationInterop.VideoStreamIndex, - 0, - null, - &flags, - ×tamp, - &sample); - if (result.Failed) - { - if (stopToken.IsCancellationRequested) - { - break; - } - MediaFoundationInterop.ThrowIfFailed(result, "IMFSourceReader.ReadSample"); - } - - try - { - if (stopToken.IsCancellationRequested) - { - break; - } - var readerFlags = (MF_SOURCE_READER_FLAG)flags; - if ((readerFlags & (MF_SOURCE_READERF_ERROR | - MF_SOURCE_READERF_ENDOFSTREAM | - MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED | - MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)) != 0) - { - throw new InvalidOperationException( - $"FlashCap: Media Foundation capture stream changed state ({readerFlags})."); - } - if (sample is null || (readerFlags & MF_SOURCE_READERF_STREAMTICK) != 0) - { - continue; - } - - firstTimestamp ??= timestamp; - this.ProcessSample( - sample, - defaultStride, - Math.Max(0, timestamp - firstTimestamp.Value) / 10, - frameIndex++); - } - finally - { - MediaFoundationInterop.Release(sample); - } - } - } - - private unsafe void ProcessSample(IMFSample* sample, int? defaultStride, long timestamp, long frameIndex) + private unsafe void OnFrame( + byte* data, + int length, + int? defaultStride, + long timestampMicroseconds, + long frameIndex) { - IMFMediaBuffer* buffer = null; - MediaFoundationInterop.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; + var frame = this.NormalizeFrame(data, length, defaultStride); try { - uint currentLength = 0; - MediaFoundationInterop.ThrowIfFailed( - buffer->Lock(&data, null, ¤tLength), - "IMFMediaBuffer.Lock"); - locked = true; - if (data is null || currentLength == 0 || currentLength > int.MaxValue) + Debug.Assert(this.frameProcessor is not null); + if (frame.Pointer != IntPtr.Zero) { - throw new InvalidOperationException("FlashCap: Media Foundation returned an invalid frame buffer."); + this.frameProcessor.OnFrameArrived( + this, frame.Pointer, frame.Length, timestampMicroseconds, frameIndex); } - - var frame = this.NormalizeFrame(data, checked((int)currentLength), defaultStride); - try + else { - if (frame.Pointer != IntPtr.Zero) + fixed (byte* repacked = this.repackBuffer!) { - this.frameProcessor!.OnFrameArrived( - this, frame.Pointer, frame.Length, timestamp, frameIndex); - } - else - { - fixed (byte* repacked = this.repackBuffer!) - { - this.frameProcessor!.OnFrameArrived( - this, (IntPtr)repacked, frame.Length, timestamp, frameIndex); - } + this.frameProcessor.OnFrameArrived( + this, (IntPtr)repacked, frame.Length, timestampMicroseconds, frameIndex); } } - catch (Exception exception) - { - MediaFoundationInterop.TraceFailure("frame callback", exception); - } } - finally + catch (Exception exception) { - if (locked) - { - _ = buffer->Unlock(); - } - MediaFoundationInterop.Release(buffer); + MediaFoundationHelpers.TraceFailure("frame callback", exception); } } @@ -512,7 +373,7 @@ private static unsafe void Interrupt(IMFSourceReader* reader) { if (reader is not null) { - MediaFoundationInterop.ThrowIfFailed( + MediaFoundationHelpers.ThrowIfFailed( reader->Flush(unchecked((uint)MF_SOURCE_READER_ALL_STREAMS)), "IMFSourceReader.Flush"); } @@ -523,20 +384,14 @@ private static unsafe void Interrupt(IMFSourceReader* reader) } } - protected override unsafe void OnCapture( + protected override void OnCapture( IntPtr pData, int size, long timestampMicroseconds, long frameIndex, PixelBuffer buffer) { - buffer.CopyIn( - this.bitmapHeader, - pData, - size, - timestampMicroseconds, - frameIndex, - this.transcodeFormat); + buffer.CopyIn(this.bitmapHeader, pData, size, timestampMicroseconds, frameIndex, this.transcodeFormat); } private readonly record struct FrameMemory(IntPtr Pointer, int Length); diff --git a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs index ee79c3b..a7ea785 100644 --- a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs @@ -14,6 +14,7 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using FlashCap.Internal.MediaFoundation; namespace FlashCap.Devices; diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index 98bbefa..aa682ed 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -16,6 +16,7 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using FlashCap.Internal.MediaFoundation; namespace FlashCap.Devices; @@ -52,7 +53,7 @@ protected override IEnumerable OnEnumerateDescriptors() } catch (Exception exception) { - MediaFoundationInterop.TraceFailure("device discovery", exception); + MediaFoundationHelpers.TraceFailure("device discovery", exception); return []; } } diff --git a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs new file mode 100644 index 0000000..f79ca4a --- /dev/null +++ b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs @@ -0,0 +1,234 @@ +#if NET8_0_OR_GREATER +//////////////////////////////////////////////////////////////////////////// +// +// 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.Threading; +using Windows.Win32; +using Windows.Win32.Media.MediaFoundation; +using static FlashCap.Internal.MediaFoundation.MediaFoundationInterop; + +namespace FlashCap.Internal.MediaFoundation; + +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 \ No newline at end of file diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs new file mode 100644 index 0000000..8f55d66 --- /dev/null +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs @@ -0,0 +1,31 @@ +#if NET8_0_OR_GREATER +//////////////////////////////////////////////////////////////////////////// +// +// 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 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}"); +} +#endif \ No newline at end of file diff --git a/FlashCap.Core/Internal/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs similarity index 90% rename from FlashCap.Core/Internal/MediaFoundationInterop.cs rename to FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs index a3fdc07..aebe076 100644 --- a/FlashCap.Core/Internal/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs @@ -8,7 +8,6 @@ // //////////////////////////////////////////////////////////////////////////// -using FlashCap.Utilities; using System; using System.Collections.Generic; using System.Diagnostics; @@ -19,8 +18,10 @@ 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; +namespace FlashCap.Internal.MediaFoundation; [SupportedOSPlatform("windows6.0")] internal static unsafe class MediaFoundationInterop @@ -53,14 +54,12 @@ internal readonly record struct FrameLayout( internal int TargetLength => checked(this.TargetStride * this.Rows); } - 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 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. @@ -379,31 +378,7 @@ internal static void RepackFrame( } } - 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."); - } + internal static void FreeActivateArray(IMFActivate** devices, uint count) { @@ -425,8 +400,5 @@ internal static void Release(T* value) where T : unmanaged _ = ((IUnknown*)value)->Release(); } } - - internal static void TraceFailure(string operation, Exception exception) => - Trace.WriteLine($"FlashCap: Media Foundation {operation} failed: {exception}"); } #endif From f9bbc79ae820c093679492903383d88518918fa4 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 16:12:31 +0200 Subject: [PATCH 06/18] feat: Annotate DirectShow with RequiresDynamicCode --- FlashCap.Core/CaptureDevices.cs | 57 +++++++++++++------ FlashCap.Core/Devices/DirectShowDevice.cs | 2 + .../Devices/DirectShowDeviceDescriptor.cs | 2 + FlashCap.Core/Devices/DirectShowDevices.cs | 2 + .../Internal/NativeMethods_DirectShow.cs | 2 + FlashCap.Core/Utilities/PolyFill.cs | 43 ++++++++++++++ 6 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 FlashCap.Core/Utilities/PolyFill.cs diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index 7f4dd82..2cd69c2 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -18,32 +18,55 @@ namespace FlashCap; -public class CaptureDevices(BufferPool defaultBufferPool) +/// +/// By default, the following backends are considered for : +/// +/// (Win)- Only if is true +/// (Win) +/// (Win) - Supported only >= NET8.0 or greater and Windows6.0 or greater +/// (Linux) +/// (MacOs) +/// +/// +public class CaptureDevices { - protected readonly BufferPool DefaultBufferPool = defaultBufferPool; + protected readonly BufferPool DefaultBufferPool; public CaptureDevices() : this(new DefaultBufferPool()) { } - protected virtual IEnumerable OnEnumerateDescriptors() => - NativeMethods.CurrentPlatform switch + public CaptureDevices(BufferPool defaultBufferPool) + { + DefaultBufferPool = defaultBufferPool; + } + + protected virtual IEnumerable OnEnumerateDescriptors() + { + switch (NativeMethods.CurrentPlatform) { - NativeMethods.Platforms.Windows => - new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(). - Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()) -#if NET8_0_OR_GREATER - .Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors()) + case NativeMethods.Platforms.Windows: + { + IEnumerable descriptors = []; +#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER + if (RuntimeFeature.IsDynamicCodeSupported) +#endif + descriptors = new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(); + descriptors = descriptors.Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); +#if FLASHCAP_MEDIAFOUNDATION + descriptors = descriptors.Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); #endif - , - NativeMethods.Platforms.Linux => - new V4L2Devices().OnEnumerateDescriptors(), - NativeMethods.Platforms.MacOS => - new AVFoundationDevices().OnEnumerateDescriptors(), - _ => - ArrayEx.Empty(), - }; + return descriptors; + } + case NativeMethods.Platforms.Linux: + return new V4L2Devices().OnEnumerateDescriptors(); + case NativeMethods.Platforms.MacOS: + return new AVFoundationDevices().OnEnumerateDescriptors(); + default: + return ArrayEx.Empty(); + } + } internal IEnumerable InternalEnumerateDescriptors() => this.OnEnumerateDescriptors(); diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 345a3b7..b6450e9 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -10,6 +10,7 @@ using FlashCap.Internal; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -18,6 +19,7 @@ namespace FlashCap.Devices; +[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] public sealed class DirectShowDevice : CaptureDevice { diff --git a/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs b/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs index 01f36a1..083fc7c 100644 --- a/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs @@ -7,11 +7,13 @@ // //////////////////////////////////////////////////////////////////////////// +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; namespace FlashCap.Devices; +[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] 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..509bc72 100644 --- a/FlashCap.Core/Devices/DirectShowDevices.cs +++ b/FlashCap.Core/Devices/DirectShowDevices.cs @@ -10,10 +10,12 @@ using FlashCap.Internal; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace FlashCap.Devices; +[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] public sealed class DirectShowDevices : CaptureDevices { public DirectShowDevices() : diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 2d1dc8d..5f84dc9 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Security; using FlashCap.Utilities; @@ -17,6 +18,7 @@ namespace FlashCap.Internal; +[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] [SuppressUnmanagedCodeSecurity] internal static class NativeMethods_DirectShow { diff --git a/FlashCap.Core/Utilities/PolyFill.cs b/FlashCap.Core/Utilities/PolyFill.cs new file mode 100644 index 0000000..7352666 --- /dev/null +++ b/FlashCap.Core/Utilities/PolyFill.cs @@ -0,0 +1,43 @@ +#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 \ No newline at end of file From 4cd844f89bf3ed680842338b94d6cf12caa697d6 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 16:49:42 +0200 Subject: [PATCH 07/18] feat: Support WMF from net5.0 --- FlashCap.Core/CaptureDevices.cs | 4 +-- .../Devices/MediaFoundationDevice.cs | 22 +++++++++------ .../MediaFoundationDeviceDescriptor.cs | 3 +- .../Devices/MediaFoundationDevices.cs | 7 ++--- FlashCap.Core/FlashCap.Core.csproj | 5 ++-- .../MediaFoundation/CaptureSession.cs | 4 +-- .../MediaFoundation/MediaFoundationHelpers.cs | 28 +++++++++++++++++-- .../MediaFoundation/MediaFoundationInterop.cs | 15 ++++++---- 8 files changed, 59 insertions(+), 29 deletions(-) diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index 2cd69c2..a261774 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -21,9 +21,9 @@ namespace FlashCap; /// /// By default, the following backends are considered for : /// -/// (Win)- Only if is true +/// (Win)- Only if RuntimeFeature.IsDynamicCodeSupported is true /// (Win) -/// (Win) - Supported only >= NET8.0 or greater and Windows6.0 or greater +/// MediaFoundationDevices (Win) - Supported on .NET 5.0 or greater and Windows 6.0 or greater /// (Linux) /// (MacOs) /// diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index 25d6e74..e2871fe 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if FLASHCAP_MEDIAFOUNDATION //////////////////////////////////////////////////////////////////////////// // // FlashCap - Independent camera capture library. @@ -20,7 +20,7 @@ using FlashCap.Internal.MediaFoundation; using static Windows.Win32.Media.MediaFoundation.MF_SOURCE_READER_CONSTANTS; #if !NET9_0_OR_GREATER -using Lock = object; +using Lock = System.Object; #endif namespace FlashCap.Devices; @@ -131,7 +131,10 @@ protected override async Task OnDisposeAsync() protected override async Task OnStartAsync(CancellationToken ct) { - ObjectDisposedException.ThrowIf(this.disposed, this); + if (this.disposed) + { + throw new ObjectDisposedException(nameof(MediaFoundationDevice)); + } Task previousCapture; bool alreadyRunning; lock (this.sync) @@ -144,9 +147,9 @@ protected override async Task OnStartAsync(CancellationToken ct) { return; } - await previousCapture.WaitAsync(ct).ConfigureAwait(false); + await MediaFoundationHelpers.WaitAsync(previousCapture, ct).ConfigureAwait(false); - var startup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var startup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var stopSource = new CancellationTokenSource(); lock (this.sync) { @@ -162,7 +165,7 @@ protected override async Task OnStartAsync(CancellationToken ct) try { - await startup.Task.WaitAsync(ct).ConfigureAwait(false); + await MediaFoundationHelpers.WaitAsync(startup.Task, ct).ConfigureAwait(false); } catch { @@ -177,7 +180,8 @@ protected override async Task OnStopAsync(CancellationToken ct) try { - await Task.WhenAll(interruptTask, captureTask).WaitAsync(ct).ConfigureAwait(false); + await MediaFoundationHelpers.WaitAsync(Task.WhenAll(interruptTask, captureTask), ct). + ConfigureAwait(false); } finally { @@ -217,7 +221,7 @@ private unsafe void PrepareStop(out Task captureTask, out Task interruptTask) } } - private unsafe void Capture(TaskCompletionSource startup, CancellationToken stopToken) + private unsafe void Capture(TaskCompletionSource startup, CancellationToken stopToken) { CaptureSession? session = null; bool initialized = false; @@ -236,7 +240,7 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToken stop } this.IsRunning = true; startupCompleted = true; - startup.TrySetResult(); + startup.TrySetResult(true); session.ReadFrames(stopToken, this.OnFrame); } catch (OperationCanceledException) when (stopToken.IsCancellationRequested) diff --git a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs index a7ea785..8f47523 100644 --- a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if FLASHCAP_MEDIAFOUNDATION //////////////////////////////////////////////////////////////////////////// // // FlashCap - Independent camera capture library. @@ -8,7 +8,6 @@ // //////////////////////////////////////////////////////////////////////////// -using FlashCap.Internal; using System.Collections.Generic; using System.Linq; using System.Runtime.Versioning; diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index aa682ed..6cf4de3 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if FLASHCAP_MEDIAFOUNDATION //////////////////////////////////////////////////////////////////////////// // // FlashCap - Independent camera capture library. @@ -8,10 +8,8 @@ // //////////////////////////////////////////////////////////////////////////// -using FlashCap.Internal; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Runtime.Versioning; using System.Threading; @@ -32,8 +30,7 @@ protected override IEnumerable OnEnumerateDescriptors() { if (!OperatingSystem.IsWindows()) { - // The class is guarded by SupportedOSPlatform - throw new UnreachableException(); + throw new PlatformNotSupportedException(); } try diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj index 99d5251..7d5e437 100644 --- a/FlashCap.Core/FlashCap.Core.csproj +++ b/FlashCap.Core/FlashCap.Core.csproj @@ -7,7 +7,8 @@ true - + + $(DefineConstants);FLASHCAP_MEDIAFOUNDATION true @@ -15,7 +16,7 @@ - + runtime; build; native; contentfiles; analyzers diff --git a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs index f79ca4a..cd8bfea 100644 --- a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs +++ b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if FLASHCAP_MEDIAFOUNDATION //////////////////////////////////////////////////////////////////////////// // // FlashCap - Independent camera capture library. @@ -231,4 +231,4 @@ public void Dispose() throw new InvalidOperationException("FlashCap: The Media Foundation device is no longer available."); } } -#endif \ No newline at end of file +#endif diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs index 8f55d66..98d1903 100644 --- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if FLASHCAP_MEDIAFOUNDATION //////////////////////////////////////////////////////////////////////////// // // FlashCap - Independent camera capture library. @@ -10,6 +10,8 @@ using System; using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; using Windows.Win32.Foundation; namespace FlashCap.Internal.MediaFoundation; @@ -27,5 +29,27 @@ internal static void ThrowIfFailed(HRESULT result, string operation) 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 \ No newline at end of file +#endif diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs index aebe076..675d9d3 100644 --- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs @@ -1,4 +1,4 @@ -#if NET8_0_OR_GREATER +#if FLASHCAP_MEDIAFOUNDATION //////////////////////////////////////////////////////////////////////////// // // FlashCap - Independent camera capture library. @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Windows.Win32; @@ -191,9 +190,15 @@ private static IReadOnlyDictionary EnumerateDev { mediaSource = ActivateMediaSource(activate); reader = CreateSourceReader(mediaSource); - return EnumerateFormats(reader) - .DistinctBy(format => format.Characteristics) - .ToDictionary(format => format.Characteristics, format => format.Key); + var formats = new Dictionary(); + foreach (var format in EnumerateFormats(reader)) + { + if (!formats.ContainsKey(format.Characteristics)) + { + formats.Add(format.Characteristics, format.Key); + } + } + return formats; } finally { From 607266330fc5720df37e303635c5b44c48747b20 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 17:35:29 +0200 Subject: [PATCH 08/18] feat: Support WMF for netstandard2.1 and net48 --- .../Devices/MediaFoundationDevice.cs | 11 ++++-- .../Devices/MediaFoundationDevices.cs | 3 +- FlashCap.Core/FlashCap.Core.csproj | 9 ++++- .../MediaFoundation/MediaFoundationInterop.cs | 27 +++++++++----- FlashCap.Core/Utilities/PolyFill.cs | 37 ++++++++++++++++++- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index e2871fe..fba0801 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -10,7 +10,6 @@ using FlashCap.Internal; using System; -using System.Diagnostics; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -302,7 +301,10 @@ private unsafe void OnFrame( var frame = this.NormalizeFrame(data, length, defaultStride); try { - Debug.Assert(this.frameProcessor is not null); + if (this.frameProcessor is null) + { + throw new InvalidOperationException("FlashCap: The frame processor is not initialized."); + } if (frame.Pointer != IntPtr.Zero) { this.frameProcessor.OnFrameArrived( @@ -363,8 +365,9 @@ private unsafe FrameMemory NormalizeFrame(byte* data, int length, int? defaultSt 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), + data, + length, + managedBuffer, layout, reverseRows); return new FrameMemory(IntPtr.Zero, layout.TargetLength); diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index 6cf4de3..e3831c2 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -28,7 +29,7 @@ public MediaFoundationDevices() : protected override IEnumerable OnEnumerateDescriptors() { - if (!OperatingSystem.IsWindows()) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new PlatformNotSupportedException(); } diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj index 7d5e437..bbea56d 100644 --- a/FlashCap.Core/FlashCap.Core.csproj +++ b/FlashCap.Core/FlashCap.Core.csproj @@ -5,9 +5,10 @@ True $(NoWarn);CS0649 true + true - + $(DefineConstants);FLASHCAP_MEDIAFOUNDATION true @@ -16,7 +17,7 @@ - + runtime; build; native; contentfiles; analyzers @@ -31,6 +32,10 @@ + + + + diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs index 675d9d3..e2d2b5e 100644 --- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Runtime.InteropServices; using System.Runtime.Versioning; using Windows.Win32; @@ -364,22 +363,32 @@ internal static FrameLayout GetFrameLayout( } internal static void RepackFrame( - ReadOnlySpan source, - Span target, + byte* source, + int sourceLength, + byte[] target, FrameLayout layout, bool reverseRows) { - if ((long)layout.SourceStride * layout.Rows > source.Length || layout.TargetLength > target.Length) + if (source is null || + (long)layout.SourceStride * layout.Rows > sourceLength || + layout.TargetLength > target.Length) { throw new ArgumentException("The frame buffer is truncated."); } - target[..layout.TargetLength].Clear(); - for (var row = 0; row < layout.Rows; row++) + Array.Clear(target, 0, layout.TargetLength); + fixed (byte* targetPointer = target) { - var sourceRow = reverseRows ? layout.Rows - row - 1 : row; - source.Slice(sourceRow * layout.SourceStride, layout.RowLength). - CopyTo(target.Slice(row * layout.TargetStride, layout.RowLength)); + for (var row = 0; row < layout.Rows; row++) + { + var sourceRow = reverseRows ? layout.Rows - row - 1 : row; + var targetOffset = row * layout.TargetStride; + Buffer.MemoryCopy( + source + sourceRow * layout.SourceStride, + targetPointer + targetOffset, + target.Length - targetOffset, + layout.RowLength); + } } } diff --git a/FlashCap.Core/Utilities/PolyFill.cs b/FlashCap.Core/Utilities/PolyFill.cs index 7352666..3b032a9 100644 --- a/FlashCap.Core/Utilities/PolyFill.cs +++ b/FlashCap.Core/Utilities/PolyFill.cs @@ -40,4 +40,39 @@ public RequiresDynamicCodeAttribute(string message) public string? Url { get; set; } } } -#endif \ No newline at end of file +#endif + +#if FLASHCAP_MEDIAFOUNDATION && !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) => + this.PlatformName = platformName; + + public string PlatformName { get; } + } +} +#endif From 53341df78c3ade30360ba30407e75de06838ecae Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 19:03:59 +0200 Subject: [PATCH 09/18] fix: Reference required dependency System.Memory --- .../Devices/MediaFoundationDevice.cs | 5 ++-- FlashCap.Core/FlashCap.Core.csproj | 4 +++ .../MediaFoundation/MediaFoundationInterop.cs | 26 ++++++------------- FlashCap.Core/NativeMethods.json | 1 - .../FlashCap.Avalonia.UI.csproj | 2 +- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index fba0801..15877d2 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -365,9 +365,8 @@ private unsafe FrameMemory NormalizeFrame(byte* data, int length, int? defaultSt format is PixelFormats.RGB15 or PixelFormats.RGB16 or PixelFormats.RGB24 or PixelFormats.RGB32 or PixelFormats.ARGB32; MediaFoundationInterop.RepackFrame( - data, - length, - managedBuffer, + new ReadOnlySpan(data, length), + managedBuffer.AsSpan(0, layout.TargetLength), layout, reverseRows); return new FrameMemory(IntPtr.Zero, layout.TargetLength); diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj index bbea56d..4c9fbd2 100644 --- a/FlashCap.Core/FlashCap.Core.csproj +++ b/FlashCap.Core/FlashCap.Core.csproj @@ -36,6 +36,10 @@ + + + + diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs index e2d2b5e..3ae5daf 100644 --- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs @@ -363,32 +363,22 @@ internal static FrameLayout GetFrameLayout( } internal static void RepackFrame( - byte* source, - int sourceLength, - byte[] target, + ReadOnlySpan source, + Span target, FrameLayout layout, bool reverseRows) { - if (source is null || - (long)layout.SourceStride * layout.Rows > sourceLength || - layout.TargetLength > target.Length) + if ((long)layout.SourceStride * layout.Rows > source.Length || layout.TargetLength > target.Length) { throw new ArgumentException("The frame buffer is truncated."); } - Array.Clear(target, 0, layout.TargetLength); - fixed (byte* targetPointer = target) + target.Slice(0, layout.TargetLength).Clear(); + for (var row = 0; row < layout.Rows; row++) { - for (var row = 0; row < layout.Rows; row++) - { - var sourceRow = reverseRows ? layout.Rows - row - 1 : row; - var targetOffset = row * layout.TargetStride; - Buffer.MemoryCopy( - source + sourceRow * layout.SourceStride, - targetPointer + targetOffset, - target.Length - targetOffset, - layout.RowLength); - } + var sourceRow = reverseRows ? layout.Rows - row - 1 : row; + source.Slice(sourceRow * layout.SourceStride, layout.RowLength). + CopyTo(target.Slice(row * layout.TargetStride, layout.RowLength)); } } diff --git a/FlashCap.Core/NativeMethods.json b/FlashCap.Core/NativeMethods.json index b85ec87..41081a0 100644 --- a/FlashCap.Core/NativeMethods.json +++ b/FlashCap.Core/NativeMethods.json @@ -3,7 +3,6 @@ "allowMarshaling": false, "friendlyOverloads": { "enabled": true, - "includePointerOverloads": true, "comOutPtrGenericOverloads": false }, "useSafeHandles": false, diff --git a/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj b/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj index 4f7ad8c..408154b 100644 --- a/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj +++ b/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net48;net8.0 true true From 45252e34a862e5a7ecd8f29c26d08efd6842aae4 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 19:07:58 +0200 Subject: [PATCH 10/18] chore: Fix xml doc comment --- FlashCap.Core/CaptureDevices.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index a261774..2f391ac 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -21,11 +21,11 @@ namespace FlashCap; /// /// By default, the following backends are considered for : /// -/// (Win)- Only if RuntimeFeature.IsDynamicCodeSupported is true -/// (Win) -/// MediaFoundationDevices (Win) - Supported on .NET 5.0 or greater and Windows 6.0 or greater -/// (Linux) -/// (MacOs) +/// (windows)- Only if RuntimeFeature.IsDynamicCodeSupported is true +/// (windows) +/// MediaFoundationDevices (windows6.0 or greater) - Supported on net48, netstandard2.1, .NET 5.0 or greater +/// (linux) +/// (macOs) /// /// public class CaptureDevices From 6d408abdfb0c1edacdbe53f78857fa3977b5e02e Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 19:18:32 +0200 Subject: [PATCH 11/18] feat: Add netstandard2.0 to the list of supported platforms for WMF --- FlashCap.Core/CaptureDevices.cs | 3 +-- FlashCap.Core/FlashCap.Core.csproj | 6 +++--- .../FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index 2f391ac..61d9a5e 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; namespace FlashCap; @@ -23,7 +22,7 @@ namespace FlashCap; /// /// (windows)- Only if RuntimeFeature.IsDynamicCodeSupported is true /// (windows) -/// MediaFoundationDevices (windows6.0 or greater) - Supported on net48, netstandard2.1, .NET 5.0 or greater +/// MediaFoundationDevices (windows6.0 or greater) - Supported on net48, netstandard2.0 or greater, .NET 5.0 or greater /// (linux) /// (macOs) /// diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj index 4c9fbd2..12c8927 100644 --- a/FlashCap.Core/FlashCap.Core.csproj +++ b/FlashCap.Core/FlashCap.Core.csproj @@ -5,7 +5,7 @@ True $(NoWarn);CS0649 true - true + true @@ -32,11 +32,11 @@ - + - + diff --git a/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj b/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj index 408154b..4f7ad8c 100644 --- a/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj +++ b/samples/FlashCap.Avalonia/FlashCap.Avalonia.UI/FlashCap.Avalonia.UI.csproj @@ -1,7 +1,7 @@  - net48;net8.0 + netstandard2.0 true true From 3a2d42f899ddbebd93b4304f20382633c41e26bc Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 19:45:20 +0200 Subject: [PATCH 12/18] build: Import the correct dependency --- FlashCap.Core/CaptureDevices.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index 61d9a5e..e3d7690 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; namespace FlashCap; From f711806a1dfcfaa4b11e62aa56114fe81b49b10d Mon Sep 17 00:00:00 2001 From: OleRoss Date: Fri, 17 Jul 2026 21:59:19 +0200 Subject: [PATCH 13/18] build: Properly apply platform specific attributes --- Directory.Build.props | 2 +- FlashCap.Core/CaptureDevices.cs | 8 ++++++-- FlashCap.Core/Devices/DirectShowDevice.cs | 2 ++ FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs | 2 ++ FlashCap.Core/Devices/DirectShowDevices.cs | 2 ++ FlashCap.Core/Devices/MediaFoundationDevice.cs | 2 +- FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs | 2 +- FlashCap.Core/Devices/MediaFoundationDevices.cs | 8 ++++---- FlashCap.Core/Devices/VideoForWindowsDevice.cs | 2 ++ FlashCap.Core/Devices/VideoForWindowsDeviceDescriptor.cs | 2 ++ FlashCap.Core/Devices/VideoForWindowsDevices.cs | 2 ++ .../Internal/IndependentSingleApartmentContext.cs | 2 ++ FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs | 2 ++ .../Internal/MediaFoundation/MediaFoundationInterop.cs | 2 +- FlashCap.Core/Internal/NativeMethods_DirectShow.cs | 2 ++ 15 files changed, 32 insertions(+), 10 deletions(-) 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/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index e3d7690..6155384 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -23,7 +23,7 @@ namespace FlashCap; /// /// (windows)- Only if RuntimeFeature.IsDynamicCodeSupported is true /// (windows) -/// MediaFoundationDevices (windows6.0 or greater) - Supported on net48, netstandard2.0 or greater, .NET 5.0 or greater +/// MediaFoundationDevices (Windows 7 or greater) - Supported on net48, netstandard2.0 or greater, .NET 5.0 or greater /// (linux) /// (macOs) /// @@ -48,6 +48,9 @@ protected virtual IEnumerable OnEnumerateDescriptors() { case NativeMethods.Platforms.Windows: { + if (!OperatingSystem.IsWindows()) + return ArrayEx.Empty(); + IEnumerable descriptors = []; #if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER if (RuntimeFeature.IsDynamicCodeSupported) @@ -55,7 +58,8 @@ protected virtual IEnumerable OnEnumerateDescriptors() descriptors = new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(); descriptors = descriptors.Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); #if FLASHCAP_MEDIAFOUNDATION - descriptors = descriptors.Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); + if (OperatingSystem.IsWindowsVersionAtLeast(6, 1)) + descriptors = descriptors.Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); #endif return descriptors; } diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index b6450e9..4fe0505 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -14,12 +14,14 @@ 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; [RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] +[SupportedOSPlatform("windows")] public sealed class DirectShowDevice : CaptureDevice { diff --git a/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs b/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs index 083fc7c..9302a90 100644 --- a/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs @@ -8,12 +8,14 @@ //////////////////////////////////////////////////////////////////////////// using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; namespace FlashCap.Devices; [RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] +[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 509bc72..11946b5 100644 --- a/FlashCap.Core/Devices/DirectShowDevices.cs +++ b/FlashCap.Core/Devices/DirectShowDevices.cs @@ -12,10 +12,12 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.Versioning; namespace FlashCap.Devices; [RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] +[SupportedOSPlatform("windows")] public sealed class DirectShowDevices : CaptureDevices { public DirectShowDevices() : diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index 15877d2..2d5d557 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -24,7 +24,7 @@ namespace FlashCap.Devices; -[SupportedOSPlatform("windows6.0")] +[SupportedOSPlatform("windows6.1")] public sealed class MediaFoundationDevice : CaptureDevice { private readonly Lock sync = new(); diff --git a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs index 8f47523..d745486 100644 --- a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs @@ -17,7 +17,7 @@ namespace FlashCap.Devices; -[SupportedOSPlatform("windows6.0")] +[SupportedOSPlatform("windows6.1")] public sealed class MediaFoundationDeviceDescriptor : CaptureDeviceDescriptor { private readonly string symbolicLink; diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index e3831c2..9df718c 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -19,7 +18,7 @@ namespace FlashCap.Devices; -[SupportedOSPlatform("windows6.0")] +[SupportedOSPlatform("windows6.1")] public sealed class MediaFoundationDevices(BufferPool defaultBufferPool) : CaptureDevices(defaultBufferPool) { public MediaFoundationDevices() : @@ -29,9 +28,10 @@ public MediaFoundationDevices() : protected override IEnumerable OnEnumerateDescriptors() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1)) { - throw new PlatformNotSupportedException(); + throw new PlatformNotSupportedException( + "Media Foundation capture requires Windows 7 or later."); } try 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..17a5025 100644 --- a/FlashCap.Core/Devices/VideoForWindowsDevices.cs +++ b/FlashCap.Core/Devices/VideoForWindowsDevices.cs @@ -11,9 +11,11 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Runtime.Versioning; namespace FlashCap.Devices; +[SupportedOSPlatform("windows")] public sealed class VideoForWindowsDevices : CaptureDevices { public VideoForWindowsDevices() : diff --git a/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs b/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs index c78c7ba..4c9e789 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 { diff --git a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs index cd8bfea..dcfbff0 100644 --- a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs +++ b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs @@ -8,6 +8,7 @@ // //////////////////////////////////////////////////////////////////////////// using System; +using System.Runtime.Versioning; using System.Threading; using Windows.Win32; using Windows.Win32.Media.MediaFoundation; @@ -15,6 +16,7 @@ namespace FlashCap.Internal.MediaFoundation; +[SupportedOSPlatform("windows6.1")] internal sealed unsafe class CaptureSession : IDisposable { private IMFActivate* activate; diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs index 3ae5daf..15d0155 100644 --- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs @@ -21,7 +21,7 @@ namespace FlashCap.Internal.MediaFoundation; -[SupportedOSPlatform("windows6.0")] +[SupportedOSPlatform("windows6.1")] internal static unsafe class MediaFoundationInterop { internal const uint VideoStreamIndex = 0; diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index 5f84dc9..c8bd85e 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -12,6 +12,7 @@ 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; @@ -19,6 +20,7 @@ namespace FlashCap.Internal; [RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] +[SupportedOSPlatform("windows")] [SuppressUnmanagedCodeSecurity] internal static class NativeMethods_DirectShow { From c0e985872927959af024a64258c2b9f752fb4f90 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Sat, 18 Jul 2026 00:59:24 +0200 Subject: [PATCH 14/18] build: Apply OsPlatform Attributes for linux and macos --- FlashCap.Core/CaptureDevices.cs | 38 ++++---- FlashCap.Core/Devices/AVFoundationDevice.cs | 2 + .../Devices/AVFoundationDeviceDescriptor.cs | 2 + FlashCap.Core/Devices/AVFoundationDevices.cs | 7 ++ .../Devices/MediaFoundationDevices.cs | 6 +- FlashCap.Core/Devices/V4L2Device.cs | 2 + FlashCap.Core/Devices/V4L2DeviceDescriptor.cs | 2 + FlashCap.Core/Devices/V4L2Devices.cs | 13 ++- .../AVFoundation/AVCaptureVideoDataOutput.cs | 2 + FlashCap.Core/Internal/BackwardCompat.cs | 96 ++++++++++++++++++- .../IndependentSingleApartmentContext.cs | 2 +- FlashCap.Core/Internal/NativeMethods.cs | 43 ++++++++- FlashCap.Core/Properties/AssemblyInfo.cs | 1 + FlashCap.Core/Utilities/PolyFill.cs | 78 --------------- 14 files changed, 188 insertions(+), 106 deletions(-) delete mode 100644 FlashCap.Core/Utilities/PolyFill.cs diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index 6155384..c1455da 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.Versioning; namespace FlashCap; @@ -44,32 +45,29 @@ public CaptureDevices(BufferPool defaultBufferPool) protected virtual IEnumerable OnEnumerateDescriptors() { - switch (NativeMethods.CurrentPlatform) + if (NativeMethods.IsWindows()) { - case NativeMethods.Platforms.Windows: - { - if (!OperatingSystem.IsWindows()) - return ArrayEx.Empty(); - - IEnumerable descriptors = []; + IEnumerable descriptors = []; #if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER - if (RuntimeFeature.IsDynamicCodeSupported) + if (RuntimeFeature.IsDynamicCodeSupported) #endif - descriptors = new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(); - descriptors = descriptors.Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); + descriptors = new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(); + descriptors = descriptors.Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); #if FLASHCAP_MEDIAFOUNDATION - if (OperatingSystem.IsWindowsVersionAtLeast(6, 1)) - descriptors = descriptors.Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); + if (NativeMethods.IsWindowsVersionAtLeast(6, 1)) + descriptors = descriptors.Concat(new MediaFoundationDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); #endif - return descriptors; - } - case NativeMethods.Platforms.Linux: - return new V4L2Devices().OnEnumerateDescriptors(); - case NativeMethods.Platforms.MacOS: - return new AVFoundationDevices().OnEnumerateDescriptors(); - default: - return ArrayEx.Empty(); + return descriptors; + } + if (NativeMethods.IsLinux()) + { + return new V4L2Devices().OnEnumerateDescriptors(); + } + if (NativeMethods.IsMacOS()) + { + return new AVFoundationDevices().OnEnumerateDescriptors(); } + return ArrayEx.Empty(); } internal IEnumerable InternalEnumerateDescriptors() => 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..7a06518 100644 --- a/FlashCap.Core/Devices/AVFoundationDevices.cs +++ b/FlashCap.Core/Devices/AVFoundationDevices.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Versioning; using System.Threading.Tasks; using FlashCap.Internal; using FlashCap.Utilities; @@ -18,6 +19,7 @@ namespace FlashCap.Devices; +[SupportedOSPlatform("macos")] public sealed class AVFoundationDevices : CaptureDevices { public AVFoundationDevices() : @@ -32,6 +34,11 @@ public AVFoundationDevices(BufferPool defaultBufferPool) : 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/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index 9df718c..683bdea 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -14,6 +14,7 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using FlashCap.Internal; using FlashCap.Internal.MediaFoundation; namespace FlashCap.Devices; @@ -28,10 +29,9 @@ public MediaFoundationDevices() : protected override IEnumerable OnEnumerateDescriptors() { - if (!OperatingSystem.IsWindowsVersionAtLeast(6, 1)) + if (!NativeMethods.IsWindowsVersionAtLeast(6, 1)) { - throw new PlatformNotSupportedException( - "Media Foundation capture requires Windows 7 or later."); + throw new PlatformNotSupportedException("Media Foundation capture requires Windows 7 or later."); } try 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..84c5abb 100644 --- a/FlashCap.Core/Devices/V4L2Devices.cs +++ b/FlashCap.Core/Devices/V4L2Devices.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.Versioning; using System.Text; using static FlashCap.Internal.NativeMethods_V4L2; @@ -20,6 +21,7 @@ namespace FlashCap.Devices; +[SupportedOSPlatform("linux")] public sealed class V4L2Devices : CaptureDevices { public V4L2Devices() : @@ -187,8 +189,14 @@ private static string ToString(byte[] data) return str; } - protected override IEnumerable OnEnumerateDescriptors() => - Directory.GetFiles("/dev", "video*"). + 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 +239,5 @@ protected override IEnumerable OnEnumerateDescriptors() return null; } }); + } } 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..7a24d1b 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,97 @@ 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; } + } +} +#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 4c9e789..5d5d863 100644 --- a/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs +++ b/FlashCap.Core/Internal/IndependentSingleApartmentContext.cs @@ -115,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/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/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")] diff --git a/FlashCap.Core/Utilities/PolyFill.cs b/FlashCap.Core/Utilities/PolyFill.cs deleted file mode 100644 index 3b032a9..0000000 --- a/FlashCap.Core/Utilities/PolyFill.cs +++ /dev/null @@ -1,78 +0,0 @@ -#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 - -#if FLASHCAP_MEDIAFOUNDATION && !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) => - this.PlatformName = platformName; - - public string PlatformName { get; } - } -} -#endif From 9f2929fc5ae9ca9cccf1f1ff3d6eb13e1b215127 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Mon, 20 Jul 2026 10:23:39 +0200 Subject: [PATCH 15/18] fix: Use UnreferencedCode instaed of DynamicCode attributes for marking COM COM Interfaces do not require dynamic code but unreferenced code. That means they are relevant for trimming as well, not just for aot --- FlashCap.Core/CaptureDevices.cs | 11 +- FlashCap.Core/Devices/AVFoundationDevices.cs | 2 + FlashCap.Core/Devices/DirectShowDevice.cs | 5 +- .../Devices/DirectShowDeviceDescriptor.cs | 4 +- FlashCap.Core/Devices/DirectShowDevices.cs | 4 +- .../Devices/MediaFoundationDevices.cs | 2 + FlashCap.Core/Devices/V4L2Devices.cs | 2 + .../Devices/VideoForWindowsDevices.cs | 2 + FlashCap.Core/Internal/BackwardCompat.cs | 104 ++++++++++++++++++ .../Internal/NativeMethods_DirectShow.cs | 9 +- 10 files changed, 134 insertions(+), 11 deletions(-) diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs index c1455da..47e0c31 100644 --- a/FlashCap.Core/CaptureDevices.cs +++ b/FlashCap.Core/CaptureDevices.cs @@ -13,6 +13,7 @@ using FlashCap.Internal; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.Versioning; @@ -22,7 +23,7 @@ namespace FlashCap; /// /// By default, the following backends are considered for : /// -/// (windows)- Only if RuntimeFeature.IsDynamicCodeSupported is true +/// (windows) /// (windows) /// MediaFoundationDevices (Windows 7 or greater) - Supported on net48, netstandard2.0 or greater, .NET 5.0 or greater /// (linux) @@ -43,15 +44,12 @@ 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()) { - IEnumerable descriptors = []; -#if NETSTANDARD2_1 || NETCOREAPP3_0_OR_GREATER - if (RuntimeFeature.IsDynamicCodeSupported) -#endif - descriptors = new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(); + var descriptors = new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors(); descriptors = descriptors.Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()); #if FLASHCAP_MEDIAFOUNDATION if (NativeMethods.IsWindowsVersionAtLeast(6, 1)) @@ -70,6 +68,7 @@ protected virtual IEnumerable 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/AVFoundationDevices.cs b/FlashCap.Core/Devices/AVFoundationDevices.cs index 7a06518..09d0bee 100644 --- a/FlashCap.Core/Devices/AVFoundationDevices.cs +++ b/FlashCap.Core/Devices/AVFoundationDevices.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.Versioning; using System.Threading.Tasks; @@ -32,6 +33,7 @@ public AVFoundationDevices(BufferPool defaultBufferPool) : { } + [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This backend does not require unreferenced code")] protected override IEnumerable OnEnumerateDescriptors() { if (!NativeMethods.IsMacOS()) diff --git a/FlashCap.Core/Devices/DirectShowDevice.cs b/FlashCap.Core/Devices/DirectShowDevice.cs index 4fe0505..c08c0c1 100644 --- a/FlashCap.Core/Devices/DirectShowDevice.cs +++ b/FlashCap.Core/Devices/DirectShowDevice.cs @@ -19,8 +19,9 @@ using System.Threading.Tasks; namespace FlashCap.Devices; - -[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] +#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 9302a90..12294e6 100644 --- a/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs +++ b/FlashCap.Core/Devices/DirectShowDeviceDescriptor.cs @@ -14,7 +14,9 @@ namespace FlashCap.Devices; -[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] +#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 { diff --git a/FlashCap.Core/Devices/DirectShowDevices.cs b/FlashCap.Core/Devices/DirectShowDevices.cs index 11946b5..1db3153 100644 --- a/FlashCap.Core/Devices/DirectShowDevices.cs +++ b/FlashCap.Core/Devices/DirectShowDevices.cs @@ -16,7 +16,9 @@ namespace FlashCap.Devices; -[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] +#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 { diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs index 683bdea..bcb0287 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevices.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.Versioning; using System.Threading; @@ -27,6 +28,7 @@ public MediaFoundationDevices() : { } + [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This backend does not require unreferenced code")] protected override IEnumerable OnEnumerateDescriptors() { if (!NativeMethods.IsWindowsVersionAtLeast(6, 1)) diff --git a/FlashCap.Core/Devices/V4L2Devices.cs b/FlashCap.Core/Devices/V4L2Devices.cs index 84c5abb..287d803 100644 --- a/FlashCap.Core/Devices/V4L2Devices.cs +++ b/FlashCap.Core/Devices/V4L2Devices.cs @@ -11,6 +11,7 @@ using FlashCap.Internal; using FlashCap.Utilities; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.Versioning; @@ -189,6 +190,7 @@ private static string ToString(byte[] data) return str; } + [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This backend does not require unreferenced code")] protected override IEnumerable OnEnumerateDescriptors() { if (!NativeMethods.IsLinux()) diff --git a/FlashCap.Core/Devices/VideoForWindowsDevices.cs b/FlashCap.Core/Devices/VideoForWindowsDevices.cs index 17a5025..27627f3 100644 --- a/FlashCap.Core/Devices/VideoForWindowsDevices.cs +++ b/FlashCap.Core/Devices/VideoForWindowsDevices.cs @@ -9,6 +9,7 @@ using FlashCap.Internal; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Runtime.Versioning; @@ -28,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/Internal/BackwardCompat.cs b/FlashCap.Core/Internal/BackwardCompat.cs index 7a24d1b..50a0b4a 100644 --- a/FlashCap.Core/Internal/BackwardCompat.cs +++ b/FlashCap.Core/Internal/BackwardCompat.cs @@ -363,6 +363,110 @@ public SupportedOSPlatformAttribute(string 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 diff --git a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs index c8bd85e..4f79cbd 100644 --- a/FlashCap.Core/Internal/NativeMethods_DirectShow.cs +++ b/FlashCap.Core/Internal/NativeMethods_DirectShow.cs @@ -19,7 +19,6 @@ namespace FlashCap.Internal; -[RequiresDynamicCode("Direct show depends on COM runtime generation and requires dynamic code")] [SupportedOSPlatform("windows")] [SuppressUnmanagedCodeSecurity] internal static class NativeMethods_DirectShow @@ -163,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) @@ -660,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( @@ -764,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. @@ -821,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) @@ -926,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( @@ -944,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. @@ -965,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. @@ -986,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( From 728ce4e9b40699d7569a9e5dce155dfad4eacb94 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Tue, 21 Jul 2026 17:47:13 +0200 Subject: [PATCH 16/18] fix: Run using an async source reader This fixes a possible race where we might be waiting on the next frame during shutdown but never receive a new one. The async sourcereader api allowes us to register a callback instead. --- .../Devices/MediaFoundationDevice.cs | 78 ++---- .../MediaFoundation/AsyncSourceReaderState.cs | 235 ++++++++++++++++++ .../MediaFoundation/CaptureSession.cs | 206 +++++++++++---- .../MediaFoundation/MediaFoundationInterop.cs | 27 +- .../Internal/NativeMethods_MediaFoundation.cs | 57 +++++ FlashCap.Core/NativeMethods.txt | 1 + 6 files changed, 491 insertions(+), 113 deletions(-) create mode 100644 FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs create mode 100644 FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index 2d5d557..1e4e460 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -15,9 +15,7 @@ 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 @@ -37,8 +35,6 @@ public sealed class MediaFoundationDevice : CaptureDevice 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( @@ -154,7 +150,6 @@ protected override async Task OnStartAsync(CancellationToken ct) { this.stopSource?.Dispose(); this.stopSource = stopSource; - this.interruptTask = Task.CompletedTask; this.captureTask = Task.Factory.StartNew( () => this.Capture(startup, stopSource.Token), CancellationToken.None, @@ -175,12 +170,11 @@ protected override async Task OnStartAsync(CancellationToken ct) protected override async Task OnStopAsync(CancellationToken ct) { - this.PrepareStop(out var captureTask, out var interruptTask); + this.PrepareStop(out var captureTask); try { - await MediaFoundationHelpers.WaitAsync(Task.WhenAll(interruptTask, captureTask), ct). - ConfigureAwait(false); + await MediaFoundationHelpers.WaitAsync(captureTask, ct).ConfigureAwait(false); } finally { @@ -193,35 +187,25 @@ await MediaFoundationHelpers.WaitAsync(Task.WhenAll(interruptTask, 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) + private void PrepareStop(out Task captureTask) { 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) { + // Runs synchronously on a dedicated MTA thread so COM initialization, + // Media Foundation lifetime, and COM uninitialization remain on the same thread. CaptureSession? session = null; bool initialized = false; bool startupCompleted = false; @@ -230,17 +214,22 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToke MediaFoundationInterop.Initialize(); initialized = true; - session = CaptureSession.Open(this.symbolicLink, this.formatKey); - stopToken.ThrowIfCancellationRequested(); + session = CaptureSession.Open(this.symbolicLink, this.formatKey, this.OnFrame); - lock (this.sync) + if (!session.Start()) { - this.activeSourceReader = session.SourceReader; + stopToken.ThrowIfCancellationRequested(); + throw new InvalidOperationException( + "FlashCap: The Media Foundation session was stopped before it could start."); } this.IsRunning = true; startupCompleted = true; startup.TrySetResult(true); - session.ReadFrames(stopToken, this.OnFrame); + + var stopTask = Task.Delay(Timeout.Infinite, stopToken); + _ = Task.WaitAny(session.StopRequested, stopTask); + session.Stop(); + session.Completion.GetAwaiter().GetResult(); } catch (OperationCanceledException) when (stopToken.IsCancellationRequested) { @@ -255,6 +244,10 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToke { startup.TrySetException(exception); } + else if (session?.FlushFailure is { } flushFailure) + { + throw flushFailure; + } else { MediaFoundationHelpers.TraceFailure("capture", exception); @@ -263,21 +256,6 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToke 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) { @@ -372,24 +350,6 @@ format is PixelFormats.RGB15 or PixelFormats.RGB16 or 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, diff --git a/FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs b/FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs new file mode 100644 index 0000000..a552084 --- /dev/null +++ b/FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs @@ -0,0 +1,235 @@ +#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.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +#if !NET9_0_OR_GREATER +using Lock = System.Object; +#endif + +namespace FlashCap.Internal.MediaFoundation; + +internal sealed class AsyncSourceReaderState +{ + private enum Lifecycle + { + Created, + Running, + StopRequested, + Flushing, + Draining, + Completed, + } + + private readonly Lock sync = new(); + private readonly Action requestSample; + private readonly Action flush; + private readonly TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource stopRequested = new(TaskCreationOptions.RunContinuationsAsynchronously); + + private Exception? failure; + private Exception? flushFailure; + private int activeCallbacks; + private Lifecycle lifecycle; + + internal AsyncSourceReaderState(Action requestSample, Action flush) + { + this.requestSample = requestSample; + this.flush = flush; + } + + internal Task Completion => this.completion.Task; + internal Task StopRequested => this.stopRequested.Task; + + internal Exception? FlushFailure + { + get + { + lock (this.sync) + { + return this.flushFailure; + } + } + } + + internal bool Start() + { + Exception? failure = null; + lock (this.sync) + { + if (this.lifecycle is not Lifecycle.Created and not Lifecycle.Running) + { + return false; + } + if (this.lifecycle == Lifecycle.Running) + { + return true; + } + + this.lifecycle = Lifecycle.Running; + try + { + this.requestSample(); + } + catch (Exception exception) + { + this.lifecycle = Lifecycle.Draining; + this.failure = exception; + failure = exception; + } + } + + if (failure is null) + { + return true; + } + + this.TryComplete(); + ExceptionDispatchInfo.Capture(failure).Throw(); + return true; + } + + internal void Stop() + { + bool shouldFlush = false; + lock (this.sync) + { + if (this.lifecycle is Lifecycle.Flushing or Lifecycle.Draining or Lifecycle.Completed) + { + return; + } + if (this.lifecycle == Lifecycle.Created) + { + this.lifecycle = Lifecycle.Draining; + } + else + { + this.lifecycle = Lifecycle.Flushing; + shouldFlush = true; + } + } + + if (shouldFlush) + { + try + { + this.flush(); + } + catch (Exception exception) + { + lock (this.sync) + { + this.flushFailure = exception; + this.failure ??= exception; + if (this.lifecycle != Lifecycle.Completed) + { + this.lifecycle = Lifecycle.Draining; + } + } + } + } + this.TryComplete(); + } + + internal bool EnterCallback(out bool processSample) + { + lock (this.sync) + { + if (this.lifecycle == Lifecycle.Completed) + { + processSample = false; + return false; + } + this.activeCallbacks++; + processSample = this.lifecycle == Lifecycle.Running; + return true; + } + } + + internal void ExitCallback(Exception? callbackFailure, bool requestNextSample) + { + bool requestStop = false; + lock (this.sync) + { + if (this.activeCallbacks <= 0) + { + throw new InvalidOperationException("No asynchronous Source Reader callback is active."); + } + + if (callbackFailure is not null) + { + this.failure ??= callbackFailure; + if (this.lifecycle is Lifecycle.Running or Lifecycle.StopRequested) + { + this.lifecycle = Lifecycle.StopRequested; + requestStop = true; + } + } + else if (requestNextSample && this.lifecycle == Lifecycle.Running) + { + try + { + this.requestSample(); + } + catch (Exception exception) + { + this.failure ??= exception; + this.lifecycle = Lifecycle.StopRequested; + requestStop = true; + } + } + this.activeCallbacks--; + } + + if (requestStop) + { + this.stopRequested.TrySetResult(true); + } + this.TryComplete(); + } + + internal void OnFlushed() + { + lock (this.sync) + { + if (this.lifecycle == Lifecycle.Flushing) + { + this.lifecycle = Lifecycle.Draining; + } + } + this.TryComplete(); + } + + private void TryComplete() + { + Exception? completionFailure; + lock (this.sync) + { + if (this.lifecycle != Lifecycle.Draining || this.activeCallbacks != 0) + { + return; + } + this.lifecycle = Lifecycle.Completed; + completionFailure = this.failure; + } + + if (completionFailure is null) + { + this.completion.TrySetResult(true); + } + else + { + this.completion.TrySetException(completionFailure); + } + } +} +#endif diff --git a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs index dcfbff0..3c80cbf 100644 --- a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs +++ b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs @@ -8,37 +8,61 @@ // //////////////////////////////////////////////////////////////////////////// using System; +using System.Runtime.InteropServices; +#if NET8_0_OR_GREATER +using System.Runtime.InteropServices.Marshalling; +#endif using System.Runtime.Versioning; using System.Threading; +using System.Threading.Tasks; using Windows.Win32; using Windows.Win32.Media.MediaFoundation; +using Windows.Win32.System.Com; using static FlashCap.Internal.MediaFoundation.MediaFoundationInterop; namespace FlashCap.Internal.MediaFoundation; [SupportedOSPlatform("windows6.1")] -internal sealed unsafe class CaptureSession : IDisposable +internal sealed unsafe partial class CaptureSession : IDisposable { private IMFActivate* activate; private IMFMediaSource* mediaSource; private IMFSourceReader* reader; private int? defaultStride; + private FrameHandler? frameHandler; + private SourceReaderCallback? callback; + private AsyncSourceReaderState? state; + private long? firstTimestamp; + private long frameIndex; private CaptureSession() { } - internal IMFSourceReader* SourceReader => this.reader; + internal Task Completion => this.state?.Completion ?? Task.CompletedTask; + internal Task StopRequested => this.state?.StopRequested ?? Task.CompletedTask; + internal Exception? FlushFailure => this.state?.FlushFailure; - internal static CaptureSession Open(string symbolicLink, FormatKey formatKey) + internal static CaptureSession Open(string symbolicLink, FormatKey formatKey, FrameHandler frameHandler) { var session = new CaptureSession(); try { + session.frameHandler = frameHandler; + session.callback = new SourceReaderCallback(session); session.activate = FindActivate(symbolicLink); session.mediaSource = ActivateMediaSource(session.activate); - session.reader = CreateSourceReader(session.mediaSource); + var callbackPointer = NativeMethods_MediaFoundation.GetComInterfaceForObject(session.callback); + try + { + session.reader = CreateSourceReader(session.mediaSource, (IUnknown*)callbackPointer); + } + finally + { + NativeMethods_MediaFoundation.ReleaseComInterface(callbackPointer); + } session.defaultStride = ConfigureReader(session.reader, formatKey); + session.state = new AsyncSourceReaderState(session.RequestSample, session.Flush); return session; } catch @@ -48,64 +72,107 @@ internal static CaptureSession Open(string symbolicLink, FormatKey formatKey) } } - internal void ReadFrames(CancellationToken stopToken, FrameHandler frameHandler) + internal bool Start() => this.state?.Start() ?? + throw new ObjectDisposedException(nameof(CaptureSession)); + + internal void Stop() => this.state?.Stop(); + + private void RequestSample() { - long? firstTimestamp = null; - long frameIndex = 0; - while (!stopToken.IsCancellationRequested) - { - uint flags = 0; - long timestamp = 0; - IMFSample* sample = null; - var result = this.reader->ReadSample( + MediaFoundationHelpers.ThrowIfFailed( + this.reader->ReadSample( VideoStreamIndex, 0, null, - &flags, - ×tamp, - &sample); - if (result.Failed) + null, + null, + null), + "IMFSourceReader.ReadSample(async)"); + } + + private void Flush() + { + MediaFoundationHelpers.ThrowIfFailed( + this.reader->Flush(VideoStreamIndex), + "IMFSourceReader.Flush(async)"); + } + + private void OnReadSample(int status, uint flags, long timestamp, IMFSample* sample) + { + var state = this.state; + if (state is null || !state.EnterCallback(out var processSample)) + { + return; + } + + Exception? failure = null; + try + { + if (!processSample) { - if (stopToken.IsCancellationRequested) - { - break; - } - MediaFoundationHelpers.ThrowIfFailed(result, "IMFSourceReader.ReadSample"); + return; } - - try + if (status < 0) { - 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; - } + throw new COMException("FlashCap: Media Foundation asynchronous read failed.", status); + } - firstTimestamp ??= timestamp; + 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 not null && + (readerFlags & MF_SOURCE_READER_FLAG.MF_SOURCE_READERF_STREAMTICK) == 0) + { + this.firstTimestamp ??= timestamp; ProcessSample( sample, this.defaultStride, - Math.Max(0, timestamp - firstTimestamp.Value) / 10, - frameIndex++, - frameHandler); - } - finally - { - Release(sample); + Math.Max(0, timestamp - this.firstTimestamp.Value) / 10, + this.frameIndex++, + this.frameHandler ?? throw new ObjectDisposedException(nameof(CaptureSession))); } } + catch (Exception exception) + { + failure = exception; + } + finally + { + state.ExitCallback(failure, requestNextSample: true); + } + } + + private void OnFlush() + { + var state = this.state; + if (state is null || !state.EnterCallback(out _)) + { + return; + } + try + { + state.OnFlushed(); + } + finally + { + state.ExitCallback(null, requestNextSample: false); + } + } + + private void OnEvent() + { + var state = this.state; + if (state is null || !state.EnterCallback(out _)) + { + return; + } + state.ExitCallback(null, requestNextSample: false); } private static int? ConfigureReader(IMFSourceReader* reader, FormatKey formatKey) @@ -191,6 +258,10 @@ private static void ProcessSample( public void Dispose() { + this.callback?.Detach(); + this.callback = null; + this.state = null; + this.frameHandler = null; if (this.reader is null && this.mediaSource is not null) { _ = this.mediaSource->Shutdown(); @@ -232,5 +303,42 @@ public void Dispose() } throw new InvalidOperationException("FlashCap: The Media Foundation device is no longer available."); } + +#if NET8_0_OR_GREATER + [GeneratedComClass] +#else + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] +#endif + private sealed partial class SourceReaderCallback : NativeMethods_MediaFoundation.IMFSourceReaderCallbackInterop + { + private CaptureSession? owner; + + internal SourceReaderCallback(CaptureSession owner) => this.owner = owner; + + public int OnReadSample(int status, uint streamIndex, uint streamFlags, long timestamp, IntPtr sample) + { + Volatile.Read(ref this.owner)?.OnReadSample( + status, + streamFlags, + timestamp, + (IMFSample*)sample); + return 0; + } + + public int OnFlush(uint streamIndex) + { + Volatile.Read(ref this.owner)?.OnFlush(); + return 0; + } + + public int OnEvent(uint streamIndex, IntPtr mediaEvent) + { + Volatile.Read(ref this.owner)?.OnEvent(); + return 0; + } + + internal void Detach() => Interlocked.Exchange(ref this.owner, null); + } } #endif diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs index 15d0155..e94babc 100644 --- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs @@ -212,8 +212,7 @@ private static IReadOnlyDictionary EnumerateDev internal static string GetAllocatedString(IMFActivate* activate, in Guid key) { - PWSTR value = default; - var result = activate->GetAllocatedString(in key, out value, out _); + var result = activate->GetAllocatedString(in key, out var value, out _); if (result.Failed || value.Value is null) { return string.Empty; @@ -243,11 +242,29 @@ internal static string GetAllocatedString(IMFActivate* activate, in Guid key) } internal static IMFSourceReader* CreateSourceReader(IMFMediaSource* mediaSource) + => CreateSourceReader(mediaSource, null); + + internal static IMFSourceReader* CreateSourceReader(IMFMediaSource* mediaSource, IUnknown* callback) { IMFSourceReader* reader = null; - ThrowIfFailed( - PInvoke.MFCreateSourceReaderFromMediaSource(mediaSource, null, &reader), - nameof(PInvoke.MFCreateSourceReaderFromMediaSource)); + IMFAttributes* attributes = null; + try + { + if (callback is not null) + { + ThrowIfFailed(PInvoke.MFCreateAttributes(&attributes, 1), nameof(PInvoke.MFCreateAttributes)); + ThrowIfFailed( + attributes->SetUnknown(in PInvoke.MF_SOURCE_READER_ASYNC_CALLBACK, callback), + "IMFAttributes.SetUnknown(async callback)"); + } + ThrowIfFailed( + PInvoke.MFCreateSourceReaderFromMediaSource(mediaSource, attributes, &reader), + nameof(PInvoke.MFCreateSourceReaderFromMediaSource)); + } + finally + { + Release(attributes); + } if (reader is null) { throw new InvalidOperationException("FlashCap: Media Foundation returned no source reader."); diff --git a/FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs b/FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs new file mode 100644 index 0000000..ed113e9 --- /dev/null +++ b/FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs @@ -0,0 +1,57 @@ +#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.InteropServices; +using System.Runtime.Versioning; +#if NET8_0_OR_GREATER +using System.Runtime.InteropServices.Marshalling; +#endif + +namespace FlashCap.Internal; + +[SupportedOSPlatform("windows6.1")] +public static partial class NativeMethods_MediaFoundation +{ +#if NET8_0_OR_GREATER + [GeneratedComInterface] +#else + [ComImport] + [ComVisible(true)] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +#endif + [Guid("DEEC8D99-FA1D-4D82-84C2-2C8969944867")] + public partial interface IMFSourceReaderCallbackInterop + { + [PreserveSig] + int OnReadSample(int status, uint streamIndex, uint streamFlags, long timestamp, IntPtr sample); + + [PreserveSig] + int OnFlush(uint streamIndex); + + [PreserveSig] + int OnEvent(uint streamIndex, IntPtr mediaEvent); + } + public static IntPtr GetComInterfaceForObject(IMFSourceReaderCallbackInterop callback) + { +#if NET8_0_OR_GREATER + return ComWrappers.GetOrCreateComInterfaceForObject(callback, CreateComInterfaceFlags.None); +#else + return Marshal.GetComInterfaceForObject(callback, typeof(IMFSourceReaderCallbackInterop)); +#endif + } + + public static void ReleaseComInterface(IntPtr pointer) => _ = Marshal.Release(pointer); + +#if NET8_0_OR_GREATER + private static readonly StrategyBasedComWrappers ComWrappers = new(); +#endif +} +#endif \ No newline at end of file diff --git a/FlashCap.Core/NativeMethods.txt b/FlashCap.Core/NativeMethods.txt index 301ff23..358cbe4 100644 --- a/FlashCap.Core/NativeMethods.txt +++ b/FlashCap.Core/NativeMethods.txt @@ -26,6 +26,7 @@ MF_MT_FRAME_SIZE MF_MT_MAJOR_TYPE MF_MT_SUBTYPE MF_SOURCE_READER_CONSTANTS +MF_SOURCE_READER_ASYNC_CALLBACK MF_SOURCE_READER_FLAG MFMediaType_Video MFVideoFormat_ARGB32 From a3f74396dadc7b666cdb5ec450ccbe9e9de8234c Mon Sep 17 00:00:00 2001 From: OleRoss Date: Tue, 21 Jul 2026 18:29:39 +0200 Subject: [PATCH 17/18] fix: Catch NormalizeFrame failures early When multiple callbacks are registered, following callbacks will be called even if a callback throws --- FlashCap.Core/Devices/MediaFoundationDevice.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs index 1e4e460..5a10b28 100644 --- a/FlashCap.Core/Devices/MediaFoundationDevice.cs +++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs @@ -269,16 +269,16 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToke } } - private unsafe void OnFrame( + internal unsafe void OnFrame( byte* data, int length, int? defaultStride, long timestampMicroseconds, long frameIndex) { - var frame = this.NormalizeFrame(data, length, defaultStride); try { + var frame = this.NormalizeFrame(data, length, defaultStride); if (this.frameProcessor is null) { throw new InvalidOperationException("FlashCap: The frame processor is not initialized."); From 5dbc7e5433c592a24bce0f6fc089605f30c8b737 Mon Sep 17 00:00:00 2001 From: OleRoss Date: Tue, 21 Jul 2026 18:36:41 +0200 Subject: [PATCH 18/18] chore: Use the generated video stream index constant --- .../Internal/MediaFoundation/MediaFoundationInterop.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs index e94babc..f5e9c32 100644 --- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs +++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs @@ -24,7 +24,8 @@ namespace FlashCap.Internal.MediaFoundation; [SupportedOSPlatform("windows6.1")] internal static unsafe class MediaFoundationInterop { - internal const uint VideoStreamIndex = 0; + internal const uint VideoStreamIndex = + unchecked((uint)MF_SOURCE_READER_CONSTANTS.MF_SOURCE_READER_FIRST_VIDEO_STREAM); internal readonly record struct FormatKey( uint MediaTypeIndex,