diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 94dcc1e..1170678 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -1,6 +1,6 @@
name: .NET
-on: [push]
+on: [push, pull_request]
jobs:
build:
@@ -39,6 +39,12 @@ jobs:
7.0.x
8.0.x
9.0.x
+ 10.0.x
+
+ - name: Setup native build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes clang mono-runtime zlib1g-dev
- name: Setup NuGet package reference
run: |
@@ -48,11 +54,9 @@ jobs:
#-----------------------------------------------------------------------
# Build
- - name: Build
- run: dotnet build -p:Configuration=Release -p:Platform="Any CPU" -p:RestoreNoCache=True -p:BuildIdentifier=${GITHUB_RUN_NUMBER} FlashCap.sln
-
- - name: Build NuGet packages
- run: dotnet pack -p:Configuration=Release -p:Platform="Any CPU" -p:BuildIdentifier=${GITHUB_RUN_NUMBER} -o artifacts FlashCap.sln
+ - name: Build and verify
+ run: node scripts/test-all.mjs
+ timeout-minutes: 30
#-----------------------------------------------------------------------
# Test
diff --git a/Directory.Build.props b/Directory.Build.props
index bc9df1e..6a544ca 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -50,7 +50,7 @@
-
+
diff --git a/FlashCap.Core.Tests/AsyncSourceReaderStateTests.cs b/FlashCap.Core.Tests/AsyncSourceReaderStateTests.cs
new file mode 100644
index 0000000..00da74f
--- /dev/null
+++ b/FlashCap.Core.Tests/AsyncSourceReaderStateTests.cs
@@ -0,0 +1,190 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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.MediaFoundation;
+using NUnit.Framework;
+using System;
+using System.Threading.Tasks;
+
+namespace FlashCap.Core.Tests;
+
+[TestFixture]
+public sealed class AsyncSourceReaderStateTests
+{
+ [Test]
+ public void StartRequestsFirstSampleOnlyOnce()
+ {
+ var requests = 0;
+ var state = new AsyncSourceReaderState(() => requests++, () => { });
+
+ Assert.That(state.Start(), Is.True);
+ Assert.That(state.Start(), Is.True);
+
+ Assert.That(requests, Is.EqualTo(1));
+ Assert.That(state.Completion.IsCompleted, Is.False);
+ Assert.That(state.StopRequested.IsCompleted, Is.False);
+ }
+
+ [Test]
+ public async Task StopBeforeStartCompletesWithoutFlushing()
+ {
+ var flushes = 0;
+ var state = new AsyncSourceReaderState(() => { }, () => flushes++);
+
+ state.Stop();
+ state.Stop();
+
+ Assert.That(state.Completion.IsCompletedSuccessfully, Is.True);
+ await state.Completion;
+ Assert.That(flushes, Is.EqualTo(0));
+ Assert.That(state.Start(), Is.False);
+ Assert.That(state.EnterCallback(out var processSample), Is.False);
+ Assert.That(processSample, Is.False);
+ }
+
+ [Test]
+ public async Task RunningStateRequestsSamplesAndCompletesAfterFlush()
+ {
+ var requests = 0;
+ var flushes = 0;
+ var state = new AsyncSourceReaderState(() => requests++, () => flushes++);
+
+ Assert.That(state.Start(), Is.True);
+ Assert.That(state.EnterCallback(out var processSample), Is.True);
+ Assert.That(processSample, Is.True);
+ state.ExitCallback(null, true);
+ state.Stop();
+
+ Assert.That(requests, Is.EqualTo(2));
+ Assert.That(flushes, Is.EqualTo(1));
+ Assert.That(state.Completion.IsCompleted, Is.False);
+
+ state.OnFlushed();
+ Assert.That(state.Completion.IsCompletedSuccessfully, Is.True);
+ await state.Completion;
+
+ state.Stop();
+ Assert.That(flushes, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task CompletionWaitsForActiveCallbackAfterFlush()
+ {
+ var state = new AsyncSourceReaderState(() => { }, () => { });
+ Assert.That(state.Start(), Is.True);
+ Assert.That(state.EnterCallback(out var processSample), Is.True);
+ Assert.That(processSample, Is.True);
+
+ state.Stop();
+ state.OnFlushed();
+
+ Assert.That(state.Completion.IsCompleted, Is.False);
+
+ state.ExitCallback(null, false);
+ Assert.That(state.Completion.IsCompletedSuccessfully, Is.True);
+ await state.Completion;
+ }
+
+ [Test]
+ public async Task CallbackFailureRequestsStopAndFaultsAfterFlush()
+ {
+ var failure = new InvalidOperationException("Callback failed.");
+ var state = new AsyncSourceReaderState(() => { }, () => { });
+ Assert.That(state.Start(), Is.True);
+ Assert.That(state.EnterCallback(out _), Is.True);
+
+ state.ExitCallback(failure, false);
+
+ Assert.That(state.StopRequested.IsCompletedSuccessfully, Is.True);
+ await state.StopRequested;
+ Assert.That(state.Completion.IsCompleted, Is.False);
+
+ state.Stop();
+ state.OnFlushed();
+
+ Assert.That(state.Completion.IsFaulted, Is.True);
+ var thrown = Assert.ThrowsAsync(
+ () => state.Completion);
+ Assert.That(thrown, Is.SameAs(failure));
+ }
+
+ [Test]
+ public async Task RequestingNextSampleFailureRequestsStop()
+ {
+ var requests = 0;
+ var failure = new InvalidOperationException("Request failed.");
+ var state = new AsyncSourceReaderState(
+ () =>
+ {
+ if (++requests == 2)
+ {
+ throw failure;
+ }
+ },
+ () => { });
+ Assert.That(state.Start(), Is.True);
+ Assert.That(state.EnterCallback(out _), Is.True);
+
+ state.ExitCallback(null, true);
+
+ Assert.That(state.StopRequested.IsCompletedSuccessfully, Is.True);
+ await state.StopRequested;
+ Assert.That(requests, Is.EqualTo(2));
+
+ state.Stop();
+ state.OnFlushed();
+ Assert.That(state.Completion.IsFaulted, Is.True);
+ var thrown = Assert.ThrowsAsync(
+ () => state.Completion);
+ Assert.That(thrown, Is.SameAs(failure));
+ }
+
+ [Test]
+ public void StartPropagatesRequestFailureAndFaultsCompletion()
+ {
+ var failure = new InvalidOperationException("Startup request failed.");
+ var state = new AsyncSourceReaderState(() => throw failure, () => { });
+
+ var thrown = Assert.Throws(() => state.Start());
+ Assert.That(thrown, Is.SameAs(failure));
+
+ Assert.That(state.Completion.IsFaulted, Is.True);
+ thrown = Assert.ThrowsAsync(
+ () => state.Completion);
+ Assert.That(thrown, Is.SameAs(failure));
+ Assert.That(state.StopRequested.IsCompleted, Is.False);
+ }
+
+ [Test]
+ public void FlushFailureIsReportedAndFaultsCompletion()
+ {
+ var failure = new InvalidOperationException("Flush failed.");
+ var state = new AsyncSourceReaderState(() => { }, () => throw failure);
+ Assert.That(state.Start(), Is.True);
+
+ state.Stop();
+
+ Assert.That(state.FlushFailure, Is.SameAs(failure));
+ Assert.That(state.Completion.IsFaulted, Is.True);
+ var thrown = Assert.ThrowsAsync(
+ () => state.Completion);
+ Assert.That(thrown, Is.SameAs(failure));
+ }
+
+ [Test]
+ public void ExitCallbackRequiresActiveCallback()
+ {
+ var state = new AsyncSourceReaderState(() => { }, () => { });
+
+ var exception = Assert.Throws(() =>
+ state.ExitCallback(null, false));
+
+ Assert.That(exception!.Message, Is.EqualTo("No asynchronous Source Reader callback is active."));
+ }
+}
diff --git a/FlashCap.Core.Tests/FlashCap.Core.Tests.csproj b/FlashCap.Core.Tests/FlashCap.Core.Tests.csproj
new file mode 100644
index 0000000..ee4b8ed
--- /dev/null
+++ b/FlashCap.Core.Tests/FlashCap.Core.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0;net10.0
+ FlashCap.Core.Tests
+ true
+ false
+ true
+ true
+ Exe
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FlashCap.Core.Tests/MediaFoundationFrameTests.cs b/FlashCap.Core.Tests/MediaFoundationFrameTests.cs
new file mode 100644
index 0000000..2b8e262
--- /dev/null
+++ b/FlashCap.Core.Tests/MediaFoundationFrameTests.cs
@@ -0,0 +1,168 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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.MediaFoundation;
+using NUnit.Framework;
+using System;
+
+#pragma warning disable CA1416 // These pure helpers do not invoke Windows APIs.
+
+namespace FlashCap.Core.Tests;
+
+[TestFixture]
+public sealed class MediaFoundationFrameTests
+{
+ [TestCase(PixelFormats.RGB24, 3, 2, 24, 9, 2, 12, 12, true)]
+ [TestCase(PixelFormats.RGB32, 2, 2, 16, 8, 2, 8, 8, true)]
+ [TestCase(PixelFormats.ARGB32, 2, 2, 16, 8, 2, 8, 8, true)]
+ [TestCase(PixelFormats.RGB15, 3, 2, 12, 6, 2, 6, 8, true)]
+ [TestCase(PixelFormats.RGB16, 3, 2, 12, 6, 2, 6, 8, true)]
+ [TestCase(PixelFormats.UYVY, 3, 2, 12, 6, 2, 6, 6, false)]
+ [TestCase(PixelFormats.YUYV, 3, 2, 12, 6, 2, 6, 6, false)]
+ [TestCase(PixelFormats.NV12, 4, 3, 20, 4, 5, 4, 4, false)]
+ public void GetFrameLayoutCalculatesFormatSpecificLayout(
+ PixelFormats format,
+ int width,
+ int height,
+ int bufferLength,
+ int expectedRowLength,
+ int expectedRows,
+ int expectedSourceStride,
+ int expectedTargetStride,
+ bool expectedBottomUp)
+ {
+ var layout = MediaFoundationInterop.GetFrameLayout(
+ format,
+ width,
+ height,
+ null,
+ bufferLength);
+
+ Assert.That(layout.RowLength, Is.EqualTo(expectedRowLength));
+ Assert.That(layout.Rows, Is.EqualTo(expectedRows));
+ Assert.That(layout.SourceStride, Is.EqualTo(expectedSourceStride));
+ Assert.That(layout.TargetStride, Is.EqualTo(expectedTargetStride));
+ Assert.That(layout.BottomUp, Is.EqualTo(expectedBottomUp));
+ Assert.That(layout.TargetLength, Is.EqualTo(checked(expectedTargetStride * expectedRows)));
+ }
+
+ [TestCase(12, false)]
+ [TestCase(-12, true)]
+ public void GetFrameLayoutUsesExplicitStrideDirection(int stride, bool expectedBottomUp)
+ {
+ var layout = MediaFoundationInterop.GetFrameLayout(
+ PixelFormats.RGB24,
+ 3,
+ 2,
+ stride,
+ 24);
+
+ Assert.That(layout.SourceStride, Is.EqualTo(12));
+ Assert.That(layout.BottomUp, Is.EqualTo(expectedBottomUp));
+ }
+
+ [Test]
+ public void GetFrameLayoutRejectsUnsupportedFormat()
+ {
+ _ = Assert.Throws(() =>
+ MediaFoundationInterop.GetFrameLayout(PixelFormats.JPEG, 2, 2, null, 4));
+ }
+
+ [Test]
+ public void GetFrameLayoutRejectsTruncatedBuffer()
+ {
+ var exception = Assert.Throws(() =>
+ MediaFoundationInterop.GetFrameLayout(PixelFormats.RGB24, 2, 2, 8, 15));
+
+ Assert.That(exception!.ParamName, Is.EqualTo("bufferLength"));
+ }
+
+ [Test]
+ public void GetFrameLayoutChecksRowLengthOverflow()
+ {
+ _ = Assert.Throws(() =>
+ MediaFoundationInterop.GetFrameLayout(
+ PixelFormats.RGB32,
+ int.MaxValue,
+ 1,
+ null,
+ int.MaxValue));
+ }
+
+ [Test]
+ public unsafe void RepackFrameCopiesRowsAndClearsPadding()
+ {
+ var source = new byte[] { 1, 2, 3, 99, 4, 5, 6, 99 };
+ var target = new byte[] { 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc };
+ var layout = new MediaFoundationInterop.FrameLayout(3, 2, 4, 4, false);
+
+ fixed (byte* sourcePointer = source)
+ {
+ MediaFoundationInterop.RepackFrame(
+ sourcePointer, source.Length, target, layout, false);
+ }
+
+ Assert.That(
+ target,
+ Is.EqualTo(new byte[] { 1, 2, 3, 0, 4, 5, 6, 0, 0xcc }));
+ }
+
+ [Test]
+ public unsafe void RepackFrameReversesRowsWhenRequested()
+ {
+ var source = new byte[] { 1, 2, 3, 99, 4, 5, 6, 99 };
+ var target = new byte[8];
+ var layout = new MediaFoundationInterop.FrameLayout(3, 2, 4, 4, false);
+
+ fixed (byte* sourcePointer = source)
+ {
+ MediaFoundationInterop.RepackFrame(
+ sourcePointer, source.Length, target, layout, true);
+ }
+
+ Assert.That(
+ target,
+ Is.EqualTo(new byte[] { 4, 5, 6, 0, 1, 2, 3, 0 }));
+ }
+
+ [Test]
+ public unsafe void RepackFrameRejectsTruncatedSource()
+ {
+ var layout = new MediaFoundationInterop.FrameLayout(3, 2, 4, 4, false);
+
+ var source = new byte[7];
+ _ = Assert.Throws(() =>
+ RepackFrame(source, new byte[8], layout, false));
+ }
+
+ [Test]
+ public unsafe void RepackFrameRejectsTruncatedTarget()
+ {
+ var layout = new MediaFoundationInterop.FrameLayout(3, 2, 4, 4, false);
+
+ var source = new byte[8];
+ _ = Assert.Throws(() =>
+ RepackFrame(source, new byte[7], layout, false));
+ }
+
+ private static unsafe void RepackFrame(
+ byte[] source,
+ byte[] target,
+ MediaFoundationInterop.FrameLayout layout,
+ bool reverseRows)
+ {
+ fixed (byte* sourcePointer = source)
+ {
+ MediaFoundationInterop.RepackFrame(
+ sourcePointer, source.Length, target, layout, reverseRows);
+ }
+ }
+}
+
+#pragma warning restore CA1416
diff --git a/FlashCap.Core.Tests/MediaFoundationPixelFormatTests.cs b/FlashCap.Core.Tests/MediaFoundationPixelFormatTests.cs
new file mode 100644
index 0000000..f0d464d
--- /dev/null
+++ b/FlashCap.Core.Tests/MediaFoundationPixelFormatTests.cs
@@ -0,0 +1,61 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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.MediaFoundation;
+using NUnit.Framework;
+using System;
+
+#pragma warning disable CA1416 // These pure helpers do not invoke Windows APIs.
+
+namespace FlashCap.Core.Tests;
+
+[TestFixture]
+public sealed class MediaFoundationPixelFormatTests
+{
+ [TestCase("00000014-0000-0010-8000-00aa00389b71", PixelFormats.RGB24, "RGB24")]
+ [TestCase("00000016-0000-0010-8000-00aa00389b71", PixelFormats.RGB32, "RGB32")]
+ [TestCase("00000015-0000-0010-8000-00aa00389b71", PixelFormats.ARGB32, "ARGB32")]
+ [TestCase("00000018-0000-0010-8000-00aa00389b71", PixelFormats.RGB15, "RGB555")]
+ [TestCase("00000017-0000-0010-8000-00aa00389b71", PixelFormats.RGB16, "RGB565")]
+ [TestCase("47504a4d-0000-0010-8000-00aa00389b71", PixelFormats.JPEG, "MJPG")]
+ [TestCase("59565955-0000-0010-8000-00aa00389b71", PixelFormats.UYVY, "UYVY")]
+ [TestCase("32595559-0000-0010-8000-00aa00389b71", PixelFormats.YUYV, "YUY2")]
+ [TestCase("3231564e-0000-0010-8000-00aa00389b71", PixelFormats.NV12, "NV12")]
+ public void TryMapPixelFormatMapsKnownMediaFoundationSubtype(
+ string subtype,
+ PixelFormats expectedFormat,
+ string expectedName)
+ {
+ var mapped = MediaFoundationInterop.TryMapPixelFormat(
+ Guid.Parse(subtype),
+ out var format,
+ out var name);
+
+ Assert.That(mapped, Is.True);
+ Assert.That(format, Is.EqualTo(expectedFormat));
+ Assert.That(name, Is.EqualTo(expectedName));
+ }
+
+ [Test]
+ public void TryMapPixelFormatRejectsUnknownSubtype()
+ {
+ var subtype = new Guid("11223344-5566-7788-99aa-bbccddeeff00");
+
+ var mapped = MediaFoundationInterop.TryMapPixelFormat(
+ subtype,
+ out var format,
+ out var name);
+
+ Assert.That(mapped, Is.False);
+ Assert.That(format, Is.EqualTo(PixelFormats.Unknown));
+ Assert.That(name, Is.EqualTo(subtype.ToString("D")));
+ }
+}
+
+#pragma warning restore CA1416
diff --git a/FlashCap.Core.Tests/NativeMethodsMediaFoundationTests.cs b/FlashCap.Core.Tests/NativeMethodsMediaFoundationTests.cs
new file mode 100644
index 0000000..c0a266c
--- /dev/null
+++ b/FlashCap.Core.Tests/NativeMethodsMediaFoundationTests.cs
@@ -0,0 +1,958 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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 NUnit.Framework;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MediaFoundation = FlashCap.Internal.NativeMethods_MediaFoundation;
+
+#pragma warning disable CA1416 // The fake vtables do not invoke Windows APIs.
+
+namespace FlashCap.Core.Tests;
+
+[TestFixture]
+[NonParallelizable]
+public sealed unsafe class NativeMethodsMediaFoundationTests
+{
+ private const int VtableLength = 64;
+ private const int ExpectedHResult = unchecked((int)0x81234567);
+ private const int UnexpectedHResult = unchecked((int)0x80004005);
+ private const uint ExpectedReleaseResult = 0xfedcba98;
+ private const uint UnexpectedReleaseResult = 0;
+
+ private static readonly Guid InputGuid =
+ new("11223344-5566-7788-99aa-bbccddeeff00");
+ private static readonly Guid SecondInputGuid =
+ new("ffeeddcc-bbaa-9988-7766-554433221100");
+ private static readonly Guid OutputGuid =
+ new("13572468-2468-1357-8642-abcdefabcdef");
+
+ private static readonly nint InputPointer = (nint)0x12345678;
+ private static readonly nint OutputPointer = (nint)0x76543210;
+ private static readonly nint OutputStringPointer = (nint)0x24681357;
+
+ private static nint expectedSelf;
+ private static nint actualSelf;
+ private static int expectedCallCount;
+ private static int unexpectedCallCount;
+ private static Guid actualGuid;
+ private static Guid actualSecondGuid;
+ private static nint actualPointer;
+ private static nint actualReservedPointer;
+ private static nint actualStreamIndexPointer;
+ private static nint actualStreamFlagsPointer;
+ private static nint actualTimestampPointer;
+ private static nint actualSamplePointer;
+ private static uint actualUInt32;
+ private static uint actualSecondUInt32;
+ private static int actualBoolean;
+
+ private struct FakeComObject
+ {
+ internal void** Vtable;
+ }
+
+ [Test]
+ public void IUnknownReleaseUsesZeroBasedSlot2()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedRelease,
+ 2,
+ (void*)(delegate* unmanaged[Stdcall])&Release,
+ &instance);
+
+ var result = ((MediaFoundation.IUnknown*)&instance)->Release();
+
+ Assert.That(result, Is.EqualTo(ExpectedReleaseResult));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFAttributesGetUINT32UsesZeroBasedSlot7()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidUInt32Out,
+ 7,
+ (void*)(delegate* unmanaged[Stdcall])&GetUInt32,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFAttributes*)&instance)->GetUINT32(in key, out var value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(0x89abcdefu));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFAttributesGetUINT64UsesZeroBasedSlot8()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidUInt64Out,
+ 8,
+ (void*)(delegate* unmanaged[Stdcall])&GetUInt64,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFAttributes*)&instance)->GetUINT64(in key, out var value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(0x0123456789abcdeful));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFAttributesGetGUIDUsesZeroBasedSlot10()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidGuidOut,
+ 10,
+ (void*)(delegate* unmanaged[Stdcall])&GetGuid,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFAttributes*)&instance)->GetGUID(in key, out var value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(OutputGuid));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFAttributesGetAllocatedStringUsesZeroBasedSlot13()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedAllocatedString,
+ 13,
+ (void*)(delegate* unmanaged[Stdcall])&GetAllocatedString,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFAttributes*)&instance)->GetAllocatedString(
+ in key,
+ out var value,
+ out var length);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(OutputStringPointer));
+ Assert.That(length, Is.EqualTo(37u));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFActivateGetAllocatedStringUsesZeroBasedSlot13()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedAllocatedString,
+ 13,
+ (void*)(delegate* unmanaged[Stdcall])&GetAllocatedString,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFActivate*)&instance)->GetAllocatedString(
+ in key,
+ out var value,
+ out var length);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(OutputStringPointer));
+ Assert.That(length, Is.EqualTo(37u));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFMediaTypeGetUINT32UsesZeroBasedSlot7()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidUInt32Out,
+ 7,
+ (void*)(delegate* unmanaged[Stdcall])&GetUInt32,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFMediaType*)&instance)->GetUINT32(in key, out var value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(0x89abcdefu));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFMediaTypeGetUINT64UsesZeroBasedSlot8()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidUInt64Out,
+ 8,
+ (void*)(delegate* unmanaged[Stdcall])&GetUInt64,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFMediaType*)&instance)->GetUINT64(in key, out var value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(0x0123456789abcdeful));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFMediaTypeGetGUIDUsesZeroBasedSlot10()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidGuidOut,
+ 10,
+ (void*)(delegate* unmanaged[Stdcall])&GetGuid,
+ &instance);
+ var key = InputGuid;
+
+ var result = ((MediaFoundation.IMFMediaType*)&instance)->GetGUID(in key, out var value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(value, Is.EqualTo(OutputGuid));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFAttributesSetGUIDUsesZeroBasedSlot24()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidGuidIn,
+ 24,
+ (void*)(delegate* unmanaged[Stdcall])&SetGuid,
+ &instance);
+ var key = InputGuid;
+ var value = SecondInputGuid;
+
+ var result = ((MediaFoundation.IMFAttributes*)&instance)->SetGUID(in key, in value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(actualSecondGuid, Is.EqualTo(SecondInputGuid));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFAttributesSetUnknownUsesZeroBasedSlot27()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidPointerIn,
+ 27,
+ (void*)(delegate* unmanaged[Stdcall])&SetUnknown,
+ &instance);
+ var key = InputGuid;
+ var value = (MediaFoundation.IUnknown*)InputPointer;
+
+ var result = ((MediaFoundation.IMFAttributes*)&instance)->SetUnknown(in key, value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That(actualPointer, Is.EqualTo(InputPointer));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFActivateActivateObjectUsesZeroBasedSlot33()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedGuidPointerOut,
+ 33,
+ (void*)(delegate* unmanaged[Stdcall])&ActivateObject,
+ &instance);
+ var iid = InputGuid;
+
+ var result = ((MediaFoundation.IMFActivate*)&instance)->ActivateObject(
+ in iid,
+ out var value);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualGuid, Is.EqualTo(InputGuid));
+ Assert.That((nint)value, Is.EqualTo(OutputPointer));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFActivateShutdownObjectUsesZeroBasedSlot34()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ PrepareNoArguments(vtable, 34, &instance);
+
+ var result = ((MediaFoundation.IMFActivate*)&instance)->ShutdownObject();
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFMediaBufferLockUsesZeroBasedSlot3()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedLock,
+ 3,
+ (void*)(delegate* unmanaged[Stdcall])&Lock,
+ &instance);
+
+ var result = ((MediaFoundation.IMFMediaBuffer*)&instance)->Lock(
+ out var data,
+ out var maximumLength,
+ out var currentLength);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That((nint)data, Is.EqualTo(OutputPointer));
+ Assert.That(maximumLength, Is.EqualTo(4096u));
+ Assert.That(currentLength, Is.EqualTo(3072u));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFMediaBufferUnlockUsesZeroBasedSlot4()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ PrepareNoArguments(vtable, 4, &instance);
+
+ var result = ((MediaFoundation.IMFMediaBuffer*)&instance)->Unlock();
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFMediaSourceShutdownUsesZeroBasedSlot12()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ PrepareNoArguments(vtable, 12, &instance);
+
+ var result = ((MediaFoundation.IMFMediaSource*)&instance)->Shutdown();
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFSampleConvertToContiguousBufferUsesZeroBasedSlot41()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedPointerOut,
+ 41,
+ (void*)(delegate* unmanaged[Stdcall])&ConvertToContiguousBuffer,
+ &instance);
+
+ var result = ((MediaFoundation.IMFSample*)&instance)->ConvertToContiguousBuffer(
+ out var buffer);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That((nint)buffer, Is.EqualTo(OutputPointer));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFSourceReaderSetStreamSelectionUsesZeroBasedSlot4AndFourByteBool()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedUInt32Boolean,
+ 4,
+ (void*)(delegate* unmanaged[Stdcall])&SetStreamSelection,
+ &instance);
+ var reader = (MediaFoundation.IMFSourceReader*)&instance;
+
+ var result = reader->SetStreamSelection(0x10203040, true);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualUInt32, Is.EqualTo(0x10203040u));
+ Assert.That(actualBoolean, Is.EqualTo(1));
+ AssertExpectedCall();
+
+ result = reader->SetStreamSelection(0x50607080, false);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualUInt32, Is.EqualTo(0x50607080u));
+ Assert.That(actualBoolean, Is.EqualTo(0));
+ AssertExpectedCall(2);
+ }
+
+ [Test]
+ public void IMFSourceReaderGetNativeMediaTypeUsesZeroBasedSlot5()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedTwoUInt32PointerOut,
+ 5,
+ (void*)(delegate* unmanaged[Stdcall])&GetNativeMediaType,
+ &instance);
+
+ var result = ((MediaFoundation.IMFSourceReader*)&instance)->GetNativeMediaType(
+ 0x10203040,
+ 0x50607080,
+ out var mediaType);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualUInt32, Is.EqualTo(0x10203040u));
+ Assert.That(actualSecondUInt32, Is.EqualTo(0x50607080u));
+ Assert.That((nint)mediaType, Is.EqualTo(OutputPointer));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFSourceReaderSetCurrentMediaTypeUsesZeroBasedSlot7AndNullReservedPointer()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedSetCurrentMediaType,
+ 7,
+ (void*)(delegate* unmanaged[Stdcall])&SetCurrentMediaType,
+ &instance);
+ var mediaType = (MediaFoundation.IMFMediaType*)InputPointer;
+
+ var result = ((MediaFoundation.IMFSourceReader*)&instance)->SetCurrentMediaType(
+ 0x10203040,
+ mediaType);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualUInt32, Is.EqualTo(0x10203040u));
+ Assert.That(actualReservedPointer, Is.EqualTo(nint.Zero));
+ Assert.That(actualPointer, Is.EqualTo(InputPointer));
+ AssertExpectedCall();
+ }
+
+ [Test]
+ public void IMFSourceReaderReadSampleUsesZeroBasedSlot9()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedReadSample,
+ 9,
+ (void*)(delegate* unmanaged[Stdcall])&ReadSample,
+ &instance);
+
+ var reader = (MediaFoundation.IMFSourceReader*)&instance;
+ uint actualStreamIndex = 0;
+ uint streamFlags = 0;
+ long timestamp = 0;
+ MediaFoundation.IMFSample* sample = null;
+
+ var result = reader->ReadSample(
+ 0x10203040,
+ 0x50607080,
+ &actualStreamIndex,
+ &streamFlags,
+ ×tamp,
+ &sample);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualUInt32, Is.EqualTo(0x10203040u));
+ Assert.That(actualSecondUInt32, Is.EqualTo(0x50607080u));
+ Assert.That(actualStreamIndexPointer, Is.EqualTo((nint)(&actualStreamIndex)));
+ Assert.That(actualStreamFlagsPointer, Is.EqualTo((nint)(&streamFlags)));
+ Assert.That(actualTimestampPointer, Is.EqualTo((nint)(×tamp)));
+ Assert.That(actualSamplePointer, Is.EqualTo((nint)(&sample)));
+ Assert.That(actualStreamIndex, Is.EqualTo(0x90a0b0c0u));
+ Assert.That(streamFlags, Is.EqualTo(0xd0e0f000u));
+ Assert.That(timestamp, Is.EqualTo(0x0123456789abcdefL));
+ Assert.That((nint)sample, Is.EqualTo(OutputPointer));
+ AssertExpectedCall();
+
+ result = reader->ReadSample(
+ 0x11223344,
+ 0x55667788,
+ null,
+ null,
+ null,
+ null);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualUInt32, Is.EqualTo(0x11223344u));
+ Assert.That(actualSecondUInt32, Is.EqualTo(0x55667788u));
+ Assert.That(actualStreamIndexPointer, Is.EqualTo(nint.Zero));
+ Assert.That(actualStreamFlagsPointer, Is.EqualTo(nint.Zero));
+ Assert.That(actualTimestampPointer, Is.EqualTo(nint.Zero));
+ Assert.That(actualSamplePointer, Is.EqualTo(nint.Zero));
+ AssertExpectedCall(2);
+ }
+
+ [Test]
+ public void IMFSourceReaderFlushUsesZeroBasedSlot10()
+ {
+ void** vtable = stackalloc void*[VtableLength];
+ var instance = new FakeComObject { Vtable = vtable };
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedUInt32,
+ 10,
+ (void*)(delegate* unmanaged[Stdcall])&Flush,
+ &instance);
+
+ var result = ((MediaFoundation.IMFSourceReader*)&instance)->Flush(0x10203040);
+
+ Assert.That(result, Is.EqualTo(ExpectedHResult));
+ Assert.That(actualUInt32, Is.EqualTo(0x10203040u));
+ AssertExpectedCall();
+ }
+
+ private static void Prepare(
+ void** vtable,
+ void* unexpected,
+ int expectedSlot,
+ void* expected,
+ FakeComObject* instance)
+ {
+ for (var slot = 0; slot < VtableLength; slot++)
+ {
+ vtable[slot] = unexpected;
+ }
+ vtable[expectedSlot] = expected;
+ Reset(instance);
+ }
+
+ private static void PrepareNoArguments(
+ void** vtable,
+ int expectedSlot,
+ FakeComObject* instance) =>
+ Prepare(
+ vtable,
+ (void*)(delegate* unmanaged[Stdcall])&UnexpectedNoArguments,
+ expectedSlot,
+ (void*)(delegate* unmanaged[Stdcall])&NoArguments,
+ instance);
+
+ private static void Reset(FakeComObject* instance)
+ {
+ expectedSelf = (nint)instance;
+ actualSelf = nint.Zero;
+ expectedCallCount = 0;
+ unexpectedCallCount = 0;
+ actualGuid = Guid.Empty;
+ actualSecondGuid = Guid.Empty;
+ actualPointer = nint.Zero;
+ actualReservedPointer = nint.Zero;
+ actualStreamIndexPointer = nint.Zero;
+ actualStreamFlagsPointer = nint.Zero;
+ actualTimestampPointer = nint.Zero;
+ actualSamplePointer = nint.Zero;
+ actualUInt32 = 0;
+ actualSecondUInt32 = 0;
+ actualBoolean = 0;
+ }
+
+ private static void RecordExpected(void* self)
+ {
+ actualSelf = (nint)self;
+ expectedCallCount++;
+ }
+
+ private static void RecordUnexpected(void* self)
+ {
+ actualSelf = (nint)self;
+ unexpectedCallCount++;
+ }
+
+ private static void AssertExpectedCall(int expectedCount = 1)
+ {
+ Assert.That(actualSelf, Is.EqualTo(expectedSelf));
+ Assert.That(expectedCallCount, Is.EqualTo(expectedCount));
+ Assert.That(unexpectedCallCount, Is.EqualTo(0));
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static uint Release(void* self)
+ {
+ RecordExpected(self);
+ return ExpectedReleaseResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static uint UnexpectedRelease(void* self)
+ {
+ RecordUnexpected(self);
+ return UnexpectedReleaseResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int GetUInt32(void* self, Guid* key, uint* value)
+ {
+ RecordExpected(self);
+ actualGuid = *key;
+ *value = 0x89abcdef;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedGuidUInt32Out(void* self, Guid* key, uint* value)
+ {
+ RecordUnexpected(self);
+ *value = 0;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int GetUInt64(void* self, Guid* key, ulong* value)
+ {
+ RecordExpected(self);
+ actualGuid = *key;
+ *value = 0x0123456789abcdef;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedGuidUInt64Out(void* self, Guid* key, ulong* value)
+ {
+ RecordUnexpected(self);
+ *value = 0;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int GetGuid(void* self, Guid* key, Guid* value)
+ {
+ RecordExpected(self);
+ actualGuid = *key;
+ *value = OutputGuid;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedGuidGuidOut(void* self, Guid* key, Guid* value)
+ {
+ RecordUnexpected(self);
+ *value = Guid.Empty;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int GetAllocatedString(void* self, Guid* key, nint* value, uint* length)
+ {
+ RecordExpected(self);
+ actualGuid = *key;
+ *value = OutputStringPointer;
+ *length = 37;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedAllocatedString(void* self, Guid* key, nint* value, uint* length)
+ {
+ RecordUnexpected(self);
+ *value = nint.Zero;
+ *length = 0;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int SetGuid(void* self, Guid* key, Guid* value)
+ {
+ RecordExpected(self);
+ actualGuid = *key;
+ actualSecondGuid = *value;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedGuidGuidIn(void* self, Guid* key, Guid* value)
+ {
+ RecordUnexpected(self);
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int SetUnknown(void* self, Guid* key, void* value)
+ {
+ RecordExpected(self);
+ actualGuid = *key;
+ actualPointer = (nint)value;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedGuidPointerIn(void* self, Guid* key, void* value)
+ {
+ RecordUnexpected(self);
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int ActivateObject(void* self, Guid* iid, void** value)
+ {
+ RecordExpected(self);
+ actualGuid = *iid;
+ *value = (void*)OutputPointer;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedGuidPointerOut(void* self, Guid* iid, void** value)
+ {
+ RecordUnexpected(self);
+ *value = null;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int NoArguments(void* self)
+ {
+ RecordExpected(self);
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedNoArguments(void* self)
+ {
+ RecordUnexpected(self);
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int Lock(void* self, byte** data, uint* maximumLength, uint* currentLength)
+ {
+ RecordExpected(self);
+ *data = (byte*)OutputPointer;
+ *maximumLength = 4096;
+ *currentLength = 3072;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedLock(
+ void* self,
+ byte** data,
+ uint* maximumLength,
+ uint* currentLength)
+ {
+ RecordUnexpected(self);
+ *data = null;
+ *maximumLength = 0;
+ *currentLength = 0;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int ConvertToContiguousBuffer(void* self, void** buffer)
+ {
+ RecordExpected(self);
+ *buffer = (void*)OutputPointer;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedPointerOut(void* self, void** value)
+ {
+ RecordUnexpected(self);
+ *value = null;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int SetStreamSelection(void* self, uint streamIndex, int selected)
+ {
+ RecordExpected(self);
+ actualUInt32 = streamIndex;
+ actualBoolean = selected;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedUInt32Boolean(void* self, uint streamIndex, int selected)
+ {
+ RecordUnexpected(self);
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int GetNativeMediaType(
+ void* self,
+ uint streamIndex,
+ uint mediaTypeIndex,
+ void** mediaType)
+ {
+ RecordExpected(self);
+ actualUInt32 = streamIndex;
+ actualSecondUInt32 = mediaTypeIndex;
+ *mediaType = (void*)OutputPointer;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedTwoUInt32PointerOut(
+ void* self,
+ uint first,
+ uint second,
+ void** value)
+ {
+ RecordUnexpected(self);
+ *value = null;
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int SetCurrentMediaType(
+ void* self,
+ uint streamIndex,
+ void* reserved,
+ void* mediaType)
+ {
+ RecordExpected(self);
+ actualUInt32 = streamIndex;
+ actualReservedPointer = (nint)reserved;
+ actualPointer = (nint)mediaType;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedSetCurrentMediaType(
+ void* self,
+ uint streamIndex,
+ void* reserved,
+ void* mediaType)
+ {
+ RecordUnexpected(self);
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int ReadSample(
+ void* self,
+ uint streamIndex,
+ uint controlFlags,
+ uint* actualStreamIndex,
+ uint* streamFlags,
+ long* timestamp,
+ void** sample)
+ {
+ RecordExpected(self);
+ actualUInt32 = streamIndex;
+ actualSecondUInt32 = controlFlags;
+ actualStreamIndexPointer = (nint)actualStreamIndex;
+ actualStreamFlagsPointer = (nint)streamFlags;
+ actualTimestampPointer = (nint)timestamp;
+ actualSamplePointer = (nint)sample;
+ if (actualStreamIndex is not null)
+ {
+ *actualStreamIndex = 0x90a0b0c0;
+ }
+ if (streamFlags is not null)
+ {
+ *streamFlags = 0xd0e0f000;
+ }
+ if (timestamp is not null)
+ {
+ *timestamp = 0x0123456789abcdef;
+ }
+ if (sample is not null)
+ {
+ *sample = (void*)OutputPointer;
+ }
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedReadSample(
+ void* self,
+ uint streamIndex,
+ uint controlFlags,
+ uint* actualStreamIndex,
+ uint* streamFlags,
+ long* timestamp,
+ void** sample)
+ {
+ RecordUnexpected(self);
+ if (actualStreamIndex is not null)
+ {
+ *actualStreamIndex = 0;
+ }
+ if (streamFlags is not null)
+ {
+ *streamFlags = 0;
+ }
+ if (timestamp is not null)
+ {
+ *timestamp = 0;
+ }
+ if (sample is not null)
+ {
+ *sample = null;
+ }
+ return UnexpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int Flush(void* self, uint streamIndex)
+ {
+ RecordExpected(self);
+ actualUInt32 = streamIndex;
+ return ExpectedHResult;
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static int UnexpectedUInt32(void* self, uint value)
+ {
+ RecordUnexpected(self);
+ return UnexpectedHResult;
+ }
+}
+
+#pragma warning restore CA1416
diff --git a/FlashCap.Core.Tests/NativeMethodsPlatformTests.cs b/FlashCap.Core.Tests/NativeMethodsPlatformTests.cs
new file mode 100644
index 0000000..7aa5b17
--- /dev/null
+++ b/FlashCap.Core.Tests/NativeMethodsPlatformTests.cs
@@ -0,0 +1,57 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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 FlashCap.Devices;
+using NUnit.Framework;
+using System;
+using System.Linq;
+
+namespace FlashCap.Core.Tests;
+
+#pragma warning disable CA1416 // This test verifies the non-Windows guard itself.
+
+[TestFixture]
+public sealed class NativeMethodsPlatformTests
+{
+ [TestCase(5, 1, 6, 1, false)]
+ [TestCase(6, 0, 6, 1, false)]
+ [TestCase(6, 1, 6, 1, true)]
+ [TestCase(6, 2, 6, 1, true)]
+ [TestCase(10, 0, 6, 1, true)]
+ [TestCase(7, 0, 6, 1, true)]
+ [TestCase(6, int.MaxValue, 7, 0, false)]
+ [TestCase(7, 0, 6, int.MaxValue, true)]
+ public void IsVersionAtLeastComparesMajorThenMinor(
+ int versionMajor,
+ int versionMinor,
+ int requiredMajor,
+ int requiredMinor,
+ bool expected)
+ {
+ var actual = NativeMethods.IsVersionAtLeast(
+ new Version(versionMajor, versionMinor),
+ requiredMajor,
+ requiredMinor);
+
+ Assert.That(actual, Is.EqualTo(expected));
+ }
+
+ [Test]
+ public void MediaFoundationEnumerationRejectsNonWindowsBeforeNativeLoading()
+ {
+ if (NativeMethods.IsWindows())
+ {
+ return;
+ }
+
+ _ = Assert.Throws(() =>
+ new MediaFoundationDevices().InternalEnumerateDescriptors().ToArray());
+ }
+}
diff --git a/FlashCap.Core/CaptureDevices.cs b/FlashCap.Core/CaptureDevices.cs
index 47e0c31..ce06af0 100644
--- a/FlashCap.Core/CaptureDevices.cs
+++ b/FlashCap.Core/CaptureDevices.cs
@@ -25,7 +25,7 @@ namespace FlashCap;
///
/// - (windows)
/// - (windows)
-/// - MediaFoundationDevices (Windows 7 or greater) - Supported on net48, netstandard2.0 or greater, .NET 5.0 or greater
+/// - (Windows 7 or greater) - Available on all supported target frameworks
/// - (linux)
/// - (macOs)
///
diff --git a/FlashCap.Core/Devices/MediaFoundationDevice.cs b/FlashCap.Core/Devices/MediaFoundationDevice.cs
index 5a10b28..b805f10 100644
--- a/FlashCap.Core/Devices/MediaFoundationDevice.cs
+++ b/FlashCap.Core/Devices/MediaFoundationDevice.cs
@@ -34,7 +34,7 @@ public sealed class MediaFoundationDevice : CaptureDevice
private IntPtr bitmapHeader;
private byte[]? repackBuffer;
private CancellationTokenSource? stopSource;
- private Task captureTask = Task.CompletedTask;
+ private Task captureTask = TaskCompat.CompletedTask;
private bool disposed;
internal MediaFoundationDevice(
@@ -78,7 +78,7 @@ protected override Task OnInitializeAsync(
header->biCompression = compression;
header->biSizeImage = header->CalculateImageSize();
}
- return Task.CompletedTask;
+ return TaskCompat.CompletedTask;
}
protected override async Task OnDisposeAsync()
@@ -144,17 +144,14 @@ protected override async Task OnStartAsync(CancellationToken ct)
}
await MediaFoundationHelpers.WaitAsync(previousCapture, ct).ConfigureAwait(false);
- var startup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var startup = new AsyncTaskCompletionSource();
var stopSource = new CancellationTokenSource();
lock (this.sync)
{
this.stopSource?.Dispose();
this.stopSource = stopSource;
- this.captureTask = Task.Factory.StartNew(
- () => this.Capture(startup, stopSource.Token),
- CancellationToken.None,
- TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
- TaskScheduler.Default);
+ this.captureTask = MediaFoundationHelpers.StartCaptureWorker(
+ () => this.Capture(startup, stopSource.Token));
}
try
@@ -186,7 +183,7 @@ protected override async Task OnStopAsync(CancellationToken ct)
{
this.stopSource?.Dispose();
this.stopSource = null;
- this.captureTask = Task.CompletedTask;
+ this.captureTask = TaskCompat.CompletedTask;
}
}
}
@@ -202,7 +199,7 @@ private void PrepareStop(out Task captureTask)
}
}
- private unsafe void Capture(TaskCompletionSource startup, CancellationToken stopToken)
+ private unsafe void Capture(AsyncTaskCompletionSource startup, CancellationToken stopToken)
{
// Runs synchronously on a dedicated MTA thread so COM initialization,
// Media Foundation lifetime, and COM uninitialization remain on the same thread.
@@ -226,7 +223,7 @@ private unsafe void Capture(TaskCompletionSource startup, CancellationToke
startupCompleted = true;
startup.TrySetResult(true);
- var stopTask = Task.Delay(Timeout.Infinite, stopToken);
+ var stopTask = TaskCompat.Delay(Timeout.Infinite, stopToken);
_ = Task.WaitAny(session.StopRequested, stopTask);
session.Stop();
session.Completion.GetAwaiter().GetResult();
@@ -343,8 +340,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/MediaFoundationDeviceDescriptor.cs b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs
index d745486..c68d430 100644
--- a/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs
+++ b/FlashCap.Core/Devices/MediaFoundationDeviceDescriptor.cs
@@ -21,13 +21,13 @@ namespace FlashCap.Devices;
public sealed class MediaFoundationDeviceDescriptor : CaptureDeviceDescriptor
{
private readonly string symbolicLink;
- private readonly IReadOnlyDictionary characteristicToFormatLookup;
+ private readonly Dictionary characteristicToFormatLookup;
internal MediaFoundationDeviceDescriptor(
string symbolicLink,
string name,
string description,
- IReadOnlyDictionary characteristicToFormatLookup,
+ Dictionary characteristicToFormatLookup,
BufferPool defaultBufferPool) :
base(name, description, characteristicToFormatLookup.Keys.ToArray(), defaultBufferPool)
{
diff --git a/FlashCap.Core/Devices/MediaFoundationDevices.cs b/FlashCap.Core/Devices/MediaFoundationDevices.cs
index bcb0287..8eebfc4 100644
--- a/FlashCap.Core/Devices/MediaFoundationDevices.cs
+++ b/FlashCap.Core/Devices/MediaFoundationDevices.cs
@@ -48,7 +48,11 @@ protected override IEnumerable OnEnumerateDescriptors()
this.DefaultBufferPool))
.ToArray(),
CancellationToken.None,
+#if NET35 || NET40
+ TaskCreationOptions.LongRunning,
+#else
TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
+#endif
TaskScheduler.Default).GetAwaiter().GetResult();
}
catch (Exception exception)
diff --git a/FlashCap.Core/FlashCap.Core.csproj b/FlashCap.Core/FlashCap.Core.csproj
index 56d8928..783ee8b 100644
--- a/FlashCap.Core/FlashCap.Core.csproj
+++ b/FlashCap.Core/FlashCap.Core.csproj
@@ -5,24 +5,13 @@
True
$(NoWarn);CS0649
true
- true
-
-
-
$(DefineConstants);FLASHCAP_MEDIAFOUNDATION
- true
-
-
- runtime; build; native; contentfiles; analyzers
-
-
-
@@ -32,14 +21,6 @@
-
-
-
-
-
-
-
-
diff --git a/FlashCap.Core/Internal/BackwardCompat.cs b/FlashCap.Core/Internal/BackwardCompat.cs
index 50a0b4a..7e07465 100644
--- a/FlashCap.Core/Internal/BackwardCompat.cs
+++ b/FlashCap.Core/Internal/BackwardCompat.cs
@@ -244,6 +244,9 @@ internal static class TaskCompat
public static Task WhenAny(params Task[] tasks) =>
Task.WhenAny(tasks);
+ public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken) =>
+ Task.Delay(millisecondsDelay, cancellationToken);
+
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
@@ -265,6 +268,9 @@ public static Task FromResult(T value) =>
public static Task WhenAny(params Task[] tasks) =>
TaskEx.WhenAny(tasks);
+
+ public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken) =>
+ TaskEx.Delay(millisecondsDelay, cancellationToken);
}
}
@@ -290,6 +296,64 @@ public static ExceptionDispatchInfo Capture(Exception ex) =>
}
#endif
+namespace System.Threading.Tasks
+{
+ internal sealed class AsyncTaskCompletionSource
+ {
+ private readonly TaskCompletionSource source =
+#if NET35 || NET40 || NET45
+ new TaskCompletionSource();
+ private int completionReserved;
+#else
+ new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+#endif
+
+ public Task Task => this.source.Task;
+
+ public bool TrySetResult(T result)
+ {
+#if NET35 || NET40 || NET45
+ return this.TryQueueCompletion(() => this.source.TrySetResult(result));
+#else
+ return this.source.TrySetResult(result);
+#endif
+ }
+
+ public bool TrySetException(Exception exception)
+ {
+#if NET35 || NET40 || NET45
+ return this.TryQueueCompletion(() => this.source.TrySetException(exception));
+#else
+ return this.source.TrySetException(exception);
+#endif
+ }
+
+ public bool TrySetCanceled(CancellationToken cancellationToken)
+ {
+#if NET35 || NET40 || NET45
+ return this.TryQueueCompletion(() => this.source.TrySetCanceled());
+#else
+ return this.source.TrySetCanceled(cancellationToken);
+#endif
+ }
+
+#if NET35 || NET40 || NET45
+ private bool TryQueueCompletion(Action completion)
+ {
+ if (Interlocked.CompareExchange(ref this.completionReserved, 1, 0) != 0)
+ {
+ return false;
+ }
+
+ // These TPL versions cannot prevent inline continuations, so publish completion
+ // from another worker instead of the Media Foundation capture or callback thread.
+ ThreadPool.QueueUserWorkItem(_ => completion());
+ return true;
+ }
+#endif
+ }
+}
+
#if NETSTANDARD1_3
namespace System.Threading.Tasks
{
diff --git a/FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs b/FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs
index a552084..7a32471 100644
--- a/FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs
+++ b/FlashCap.Core/Internal/MediaFoundation/AsyncSourceReaderState.cs
@@ -33,8 +33,8 @@ private enum Lifecycle
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 readonly AsyncTaskCompletionSource completion = new();
+ private readonly AsyncTaskCompletionSource stopRequested = new();
private Exception? failure;
private Exception? flushFailure;
diff --git a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs
index 3c80cbf..6df1f9c 100644
--- a/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs
+++ b/FlashCap.Core/Internal/MediaFoundation/CaptureSession.cs
@@ -15,9 +15,8 @@
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 FlashCap.Internal;
+using static FlashCap.Internal.NativeMethods_MediaFoundation;
using static FlashCap.Internal.MediaFoundation.MediaFoundationInterop;
namespace FlashCap.Internal.MediaFoundation;
@@ -39,8 +38,8 @@ private CaptureSession()
{
}
- internal Task Completion => this.state?.Completion ?? Task.CompletedTask;
- internal Task StopRequested => this.state?.StopRequested ?? Task.CompletedTask;
+ internal Task Completion => this.state?.Completion ?? TaskCompat.CompletedTask;
+ internal Task StopRequested => this.state?.StopRequested ?? TaskCompat.CompletedTask;
internal Exception? FlushFailure => this.state?.FlushFailure;
internal static CaptureSession Open(string symbolicLink, FormatKey formatKey, FrameHandler frameHandler)
@@ -80,13 +79,7 @@ internal bool Start() => this.state?.Start() ??
private void RequestSample()
{
MediaFoundationHelpers.ThrowIfFailed(
- this.reader->ReadSample(
- VideoStreamIndex,
- 0,
- null,
- null,
- null,
- null),
+ this.reader->ReadSample(VideoStreamIndex, 0),
"IMFSourceReader.ReadSample(async)");
}
@@ -188,7 +181,7 @@ private void OnEvent()
try
{
MediaFoundationHelpers.ThrowIfFailed(
- reader->GetNativeMediaType(VideoStreamIndex, formatKey.MediaTypeIndex, &mediaType),
+ reader->GetNativeMediaType(VideoStreamIndex, formatKey.MediaTypeIndex, out mediaType),
"IMFSourceReader.GetNativeMediaType");
if (mediaType is null ||
!TryCreateFormat(mediaType, formatKey.MediaTypeIndex, out var selected) ||
@@ -201,7 +194,7 @@ private void OnEvent()
MediaFoundationHelpers.ThrowIfFailed(
reader->SetCurrentMediaType(VideoStreamIndex, mediaType),
"IMFSourceReader.SetCurrentMediaType");
- return mediaType->GetUINT32(in PInvoke.MF_MT_DEFAULT_STRIDE, out var stride).Succeeded ?
+ return mediaType->GetUINT32(in MF_MT_DEFAULT_STRIDE, out var stride) >= 0 ?
unchecked((int)stride) : null;
}
finally
@@ -219,19 +212,19 @@ private static void ProcessSample(
{
IMFMediaBuffer* buffer = null;
MediaFoundationHelpers.ThrowIfFailed(
- sample->ConvertToContiguousBuffer(&buffer),
+ sample->ConvertToContiguousBuffer(out 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");
+ MediaFoundationHelpers.ThrowIfFailed(
+ buffer->Lock(out var data, out _, out var currentLength),
+ "IMFMediaBuffer.Lock");
locked = true;
if (data is null || currentLength == 0 || currentLength > int.MaxValue)
{
@@ -289,7 +282,7 @@ public void Dispose()
{
var activate = devices[index];
if (activate is not null && string.Equals(
- GetAllocatedString(activate, in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim(),
+ GetAllocatedString(activate, in MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim(),
symbolicLink, StringComparison.OrdinalIgnoreCase))
{
devices[index] = null;
@@ -310,7 +303,7 @@ public void Dispose()
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
#endif
- private sealed partial class SourceReaderCallback : NativeMethods_MediaFoundation.IMFSourceReaderCallbackInterop
+ private sealed partial class SourceReaderCallback : IMFSourceReaderCallbackInterop
{
private CaptureSession? owner;
@@ -318,7 +311,7 @@ private sealed partial class SourceReaderCallback : NativeMethods_MediaFoundatio
public int OnReadSample(int status, uint streamIndex, uint streamFlags, long timestamp, IntPtr sample)
{
- Volatile.Read(ref this.owner)?.OnReadSample(
+ Interlocked.CompareExchange(ref this.owner, null, null)?.OnReadSample(
status,
streamFlags,
timestamp,
@@ -328,13 +321,13 @@ public int OnReadSample(int status, uint streamIndex, uint streamFlags, long tim
public int OnFlush(uint streamIndex)
{
- Volatile.Read(ref this.owner)?.OnFlush();
+ Interlocked.CompareExchange(ref this.owner, null, null)?.OnFlush();
return 0;
}
public int OnEvent(uint streamIndex, IntPtr mediaEvent)
{
- Volatile.Read(ref this.owner)?.OnEvent();
+ Interlocked.CompareExchange(ref this.owner, null, null)?.OnEvent();
return 0;
}
diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs
index 98d1903..bdb4993 100644
--- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs
+++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationHelpers.cs
@@ -12,24 +12,54 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
-using Windows.Win32.Foundation;
namespace FlashCap.Internal.MediaFoundation;
internal static class MediaFoundationHelpers
{
- internal static void ThrowIfFailed(HRESULT result, string operation)
+ internal static void ThrowIfFailed(int result, string operation)
{
- if (result.Failed)
+ if (result < 0)
{
throw new InvalidOperationException(
- $"FlashCap: {operation} failed (HRESULT=0x{unchecked((uint)result.Value):X8}).");
+ $"FlashCap: {operation} failed (HRESULT=0x{unchecked((uint)result):X8}).");
}
}
internal static void TraceFailure(string operation, Exception exception) =>
Trace.WriteLine($"FlashCap: Media Foundation {operation} failed: {exception}");
+ internal static Task StartCaptureWorker(Action capture)
+ {
+#if NET35 || NET40
+ var completion = new AsyncTaskCompletionSource();
+ // A real thread has no parent Task to which user-created child tasks could attach.
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ capture();
+ completion.TrySetResult(true);
+ }
+ catch (Exception exception)
+ {
+ completion.TrySetException(exception);
+ }
+ })
+ {
+ IsBackground = true,
+ };
+ thread.Start();
+ return completion.Task;
+#else
+ return Task.Factory.StartNew(
+ capture,
+ CancellationToken.None,
+ TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach,
+ TaskScheduler.Default);
+#endif
+ }
+
internal static async Task WaitAsync(Task task, CancellationToken ct)
{
#if NET6_0_OR_GREATER
@@ -41,10 +71,9 @@ internal static async Task WaitAsync(Task task, CancellationToken ct)
return;
}
- var cancellation = new TaskCompletionSource(
- TaskCreationOptions.RunContinuationsAsynchronously);
+ var cancellation = new AsyncTaskCompletionSource();
using var registration = ct.Register(() => cancellation.TrySetResult(true));
- if (await Task.WhenAny(task, cancellation.Task).ConfigureAwait(false) != task)
+ if (await TaskCompat.WhenAny(task, cancellation.Task).ConfigureAwait(false) != task)
{
ct.ThrowIfCancellationRequested();
}
diff --git a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs
index f5e9c32..ca067a9 100644
--- a/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs
+++ b/FlashCap.Core/Internal/MediaFoundation/MediaFoundationInterop.cs
@@ -12,11 +12,8 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
-using Windows.Win32;
-using Windows.Win32.Foundation;
-using Windows.Win32.Media.MediaFoundation;
-using Windows.Win32.System.Com;
using FlashCap.Utilities;
+using static FlashCap.Internal.NativeMethods_MediaFoundation;
using static FlashCap.Internal.MediaFoundation.MediaFoundationHelpers;
namespace FlashCap.Internal.MediaFoundation;
@@ -41,7 +38,7 @@ internal readonly record struct Format(
internal readonly record struct DeviceInfo(
string SymbolicLink,
string Name,
- IReadOnlyDictionary Formats);
+ Dictionary Formats);
internal readonly record struct FrameLayout(
int RowLength,
@@ -66,33 +63,33 @@ internal delegate void FrameHandler(
///
internal static void Initialize()
{
- var result = PInvoke.CoInitializeEx(COINIT.COINIT_MULTITHREADED);
- ThrowIfFailed(result, nameof(PInvoke.CoInitializeEx));
+ var result = NativeMethods.CoInitializeEx(IntPtr.Zero, NativeMethods.COINIT.MULTITHREADED);
+ ThrowIfFailed(result, nameof(NativeMethods.CoInitializeEx));
- result = PInvoke.MFStartup(PInvoke.MF_VERSION, PInvoke.MFSTARTUP_FULL);
- if (result.Failed)
+ result = MFStartup(MF_VERSION, MFSTARTUP_FULL);
+ if (result < 0)
{
- PInvoke.CoUninitialize();
- ThrowIfFailed(result, nameof(PInvoke.MFStartup));
+ NativeMethods.CoUninitialize();
+ ThrowIfFailed(result, nameof(MFStartup));
}
}
internal static void Uninitialize()
{
- _ = PInvoke.MFShutdown();
- PInvoke.CoUninitialize();
+ _ = MFShutdown();
+ NativeMethods.CoUninitialize();
}
internal static IMFAttributes* CreateVideoCaptureAttributes()
{
IMFAttributes* attributes = null;
- ThrowIfFailed(PInvoke.MFCreateAttributes(&attributes, 1), nameof(PInvoke.MFCreateAttributes));
+ ThrowIfFailed(MFCreateAttributes(&attributes, 1), nameof(MFCreateAttributes));
try
{
ThrowIfFailed(
attributes->SetGUID(
- in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
- in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID),
+ in MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
+ in MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID),
"IMFAttributes.SetGUID");
return attributes;
}
@@ -109,8 +106,8 @@ internal static void Uninitialize()
try
{
ThrowIfFailed(
- PInvoke.MFEnumDeviceSources(attributes, out var devices, out count),
- nameof(PInvoke.MFEnumDeviceSources));
+ MFEnumDeviceSources(attributes, out var devices, out count),
+ nameof(MFEnumDeviceSources));
return devices;
}
finally
@@ -142,7 +139,7 @@ internal static DeviceInfo[] EnumerateDevices()
{
var symbolicLink = GetAllocatedString(
activate,
- in PInvoke.MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim();
+ in MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK).Trim();
if (string.IsNullOrEmpty(symbolicLink))
{
continue;
@@ -150,7 +147,7 @@ internal static DeviceInfo[] EnumerateDevices()
var name = GetAllocatedString(
activate,
- in PInvoke.MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME).Trim();
+ in MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME).Trim();
if (string.IsNullOrEmpty(name))
{
name = "Media Foundation camera";
@@ -181,7 +178,7 @@ internal static DeviceInfo[] EnumerateDevices()
}
}
- private static IReadOnlyDictionary EnumerateDeviceFormats(
+ private static Dictionary EnumerateDeviceFormats(
IMFActivate* activate)
{
IMFMediaSource* mediaSource = null;
@@ -213,26 +210,27 @@ private static IReadOnlyDictionary EnumerateDev
internal static string GetAllocatedString(IMFActivate* activate, in Guid key)
{
- var result = activate->GetAllocatedString(in key, out var value, out _);
- if (result.Failed || value.Value is null)
- {
- return string.Empty;
- }
-
+ var value = IntPtr.Zero;
try
{
- return Marshal.PtrToStringUni((IntPtr)value.Value) ?? string.Empty;
+ var result = activate->GetAllocatedString(in key, out value, out _);
+ return result < 0 || value == IntPtr.Zero ?
+ string.Empty :
+ Marshal.PtrToStringUni(value) ?? string.Empty;
}
finally
{
- Marshal.FreeCoTaskMem((IntPtr)value.Value);
+ if (value != IntPtr.Zero)
+ {
+ Marshal.FreeCoTaskMem(value);
+ }
}
}
internal static IMFMediaSource* ActivateMediaSource(IMFActivate* activate)
{
ThrowIfFailed(
- activate->ActivateObject(in IMFMediaSource.IID_Guid, out var value),
+ activate->ActivateObject(in IID_IMFMediaSource, out var value),
"IMFActivate.ActivateObject"
);
if (value is null)
@@ -253,14 +251,14 @@ internal static string GetAllocatedString(IMFActivate* activate, in Guid key)
{
if (callback is not null)
{
- ThrowIfFailed(PInvoke.MFCreateAttributes(&attributes, 1), nameof(PInvoke.MFCreateAttributes));
+ ThrowIfFailed(MFCreateAttributes(&attributes, 1), nameof(MFCreateAttributes));
ThrowIfFailed(
- attributes->SetUnknown(in PInvoke.MF_SOURCE_READER_ASYNC_CALLBACK, callback),
+ attributes->SetUnknown(in MF_SOURCE_READER_ASYNC_CALLBACK, callback),
"IMFAttributes.SetUnknown(async callback)");
}
ThrowIfFailed(
- PInvoke.MFCreateSourceReaderFromMediaSource(mediaSource, attributes, &reader),
- nameof(PInvoke.MFCreateSourceReaderFromMediaSource));
+ MFCreateSourceReaderFromMediaSource(mediaSource, attributes, &reader),
+ nameof(MFCreateSourceReaderFromMediaSource));
}
finally
{
@@ -279,8 +277,8 @@ internal static List EnumerateFormats(IMFSourceReader* reader)
for (uint index = 0; ; index++)
{
IMFMediaType* mediaType = null;
- var result = reader->GetNativeMediaType(VideoStreamIndex, index, &mediaType);
- if (result == HRESULT.MF_E_NO_MORE_TYPES)
+ var result = reader->GetNativeMediaType(VideoStreamIndex, index, out mediaType);
+ if (result == MF_E_NO_MORE_TYPES)
{
break;
}
@@ -303,11 +301,11 @@ internal static List EnumerateFormats(IMFSourceReader* reader)
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)
+ if (mediaType->GetGUID(in MF_MT_MAJOR_TYPE, out var majorType) < 0 ||
+ majorType != MFMediaType_Video ||
+ mediaType->GetGUID(in MF_MT_SUBTYPE, out var subtype) < 0 ||
+ mediaType->GetUINT64(in MF_MT_FRAME_SIZE, out var frameSize) < 0 ||
+ mediaType->GetUINT64(in MF_MT_FRAME_RATE, out var frameRate) < 0)
{
return false;
}
@@ -336,15 +334,15 @@ denominator is 0 or > int.MaxValue ||
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; }
+ if (subtype == MFVideoFormat_RGB24) { format = PixelFormats.RGB24; name = "RGB24"; return true; }
+ if (subtype == MFVideoFormat_RGB32) { format = PixelFormats.RGB32; name = "RGB32"; return true; }
+ if (subtype == MFVideoFormat_ARGB32) { format = PixelFormats.ARGB32; name = "ARGB32"; return true; }
+ if (subtype == MFVideoFormat_RGB555) { format = PixelFormats.RGB15; name = "RGB555"; return true; }
+ if (subtype == MFVideoFormat_RGB565) { format = PixelFormats.RGB16; name = "RGB565"; return true; }
+ if (subtype == MFVideoFormat_MJPG) { format = PixelFormats.JPEG; name = "MJPG"; return true; }
+ if (subtype == MFVideoFormat_UYVY) { format = PixelFormats.UYVY; name = "UYVY"; return true; }
+ if (subtype == MFVideoFormat_YUY2) { format = PixelFormats.YUYV; name = "YUY2"; return true; }
+ if (subtype == MFVideoFormat_NV12) { format = PixelFormats.NV12; name = "NV12"; return true; }
format = PixelFormats.Unknown;
name = subtype.ToString("D");
return false;
@@ -381,22 +379,47 @@ 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 (target is null)
+ {
+ throw new ArgumentNullException(nameof(target));
+ }
+
+ var sourceRequiredLength = (long)layout.SourceStride * layout.Rows;
+ var targetRequiredLength = (long)layout.TargetStride * layout.Rows;
+ if (sourceLength < 0 ||
+ layout.RowLength < 0 ||
+ layout.Rows < 0 ||
+ layout.SourceStride < layout.RowLength ||
+ layout.TargetStride < layout.RowLength ||
+ sourceRequiredLength < 0 ||
+ sourceRequiredLength > sourceLength ||
+ targetRequiredLength < 0 ||
+ targetRequiredLength > target.Length ||
+ source is null && sourceRequiredLength != 0)
{
throw new ArgumentException("The frame buffer is truncated.");
}
- target.Slice(0, layout.TargetLength).Clear();
- for (var row = 0; row < layout.Rows; row++)
+ var targetLength = checked((int)targetRequiredLength);
+ Array.Clear(target, 0, 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 sourceOffset = checked((int)((long)sourceRow * layout.SourceStride));
+ var targetOffset = checked((int)((long)row * layout.TargetStride));
+ NativeMethods.CopyMemory(
+ (IntPtr)(targetPointer + targetOffset),
+ (IntPtr)(source + sourceOffset),
+ (IntPtr)layout.RowLength);
+ }
}
}
@@ -415,12 +438,5 @@ internal static void FreeActivateArray(IMFActivate** devices, uint count)
Marshal.FreeCoTaskMem((IntPtr)devices);
}
- internal static void Release(T* value) where T : unmanaged
- {
- if (value is not null)
- {
- _ = ((IUnknown*)value)->Release();
- }
- }
}
#endif
diff --git a/FlashCap.Core/Internal/NativeMethods.cs b/FlashCap.Core/Internal/NativeMethods.cs
index f9ce3d1..c359a71 100644
--- a/FlashCap.Core/Internal/NativeMethods.cs
+++ b/FlashCap.Core/Internal/NativeMethods.cs
@@ -77,11 +77,20 @@ public static bool IsWindowsVersionAtLeast(int major, int minor = 0)
{
#if NET5_0_OR_GREATER
return OperatingSystem.IsWindowsVersionAtLeast(major, minor);
+#elif NETSTANDARD1_3
+ // The portable contract does not expose Environment.OSVersion.
+ // Media Foundation availability is verified when its entry points are invoked.
+ return CurrentPlatform == Platforms.Windows;
#else
- return CurrentPlatform == Platforms.Windows; // TODO: Find out windows revision
+ return CurrentPlatform == Platforms.Windows &&
+ IsVersionAtLeast(Environment.OSVersion.Version, major, minor);
#endif
}
+ internal static bool IsVersionAtLeast(Version version, int major, int minor) =>
+ version.Major > major ||
+ version.Major == major && version.Minor >= minor;
+
[SupportedOSPlatformGuard("linux")]
public static bool IsLinux()
{
diff --git a/FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs b/FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs
index ed113e9..7c9798e 100644
--- a/FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs
+++ b/FlashCap.Core/Internal/NativeMethods_MediaFoundation.cs
@@ -11,6 +11,7 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
+using System.Security;
#if NET8_0_OR_GREATER
using System.Runtime.InteropServices.Marshalling;
#endif
@@ -18,40 +19,544 @@
namespace FlashCap.Internal;
[SupportedOSPlatform("windows6.1")]
-public static partial class NativeMethods_MediaFoundation
+[SuppressUnmanagedCodeSecurity]
+internal static unsafe 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
+ public const uint MFSTARTUP_FULL = 0;
+ public const uint MF_VERSION = 0x00020070;
+
+ public const int MF_E_NO_MORE_TYPES = unchecked((int)0xc00d36b9);
+
+ public static readonly Guid IID_IUnknown =
+ new("00000000-0000-0000-C000-000000000046");
+ public static readonly Guid IID_IMFAttributes =
+ new("2CD2D921-C447-44A7-A13C-4ADABFC247E3");
+ public static readonly Guid IID_IMFActivate =
+ new("7FEE9E9A-4A89-47A6-899C-B6A53A70FB67");
+ public static readonly Guid IID_IMFMediaBuffer =
+ new("045FA593-8799-42B8-BC8D-8968C6453507");
+ public static readonly Guid IID_IMFMediaSource =
+ new("279A808D-AEC7-40C8-9C6B-A6B492C78A66");
+ public static readonly Guid IID_IMFMediaType =
+ new("44AE0FA8-EA31-4109-8D2E-4CAE4997C555");
+ public static readonly Guid IID_IMFSample =
+ new("C40A00F2-B93A-4D80-AE8C-5A1C634F58E4");
+ public static readonly Guid IID_IMFSourceReader =
+ new("70AE66F2-C809-4E4F-8915-BDCB406B7993");
+
+ public static readonly Guid MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME =
+ new(0x60d0e559, 0x52f8, 0x4fa2, 0xbb, 0xce, 0xac, 0xdb, 0x34, 0xa8, 0xec, 0x01);
+ public static readonly Guid MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE =
+ new(0xc60ac5fe, 0x252a, 0x478f, 0xa0, 0xef, 0xbc, 0x8f, 0xa5, 0xf7, 0xca, 0xd3);
+ public static readonly Guid MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID =
+ new(0x8ac3587a, 0x4ae7, 0x42d8, 0x99, 0xe0, 0x0a, 0x60, 0x13, 0xee, 0xf9, 0x0f);
+ public static readonly Guid MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK =
+ new(0x58f0aad8, 0x22bf, 0x4f8a, 0xbb, 0x3d, 0xd2, 0xc4, 0x97, 0x8c, 0x6e, 0x2f);
+ public static readonly Guid MF_MT_DEFAULT_STRIDE =
+ new(0x644b4e48, 0x1e02, 0x4516, 0xb0, 0xeb, 0xc0, 0x1c, 0xa9, 0xd4, 0x9a, 0xc6);
+ public static readonly Guid MF_MT_FRAME_RATE =
+ new(0xc459a2e8, 0x3d2c, 0x4e44, 0xb1, 0x32, 0xfe, 0xe5, 0x15, 0x6c, 0x7b, 0xb0);
+ public static readonly Guid MF_MT_FRAME_SIZE =
+ new(0x1652c33d, 0xd6b2, 0x4012, 0xb8, 0x34, 0x72, 0x03, 0x08, 0x49, 0xa3, 0x7d);
+ public static readonly Guid MF_MT_MAJOR_TYPE =
+ new(0x48eba18e, 0xf8c9, 0x4687, 0xbf, 0x11, 0x0a, 0x74, 0xc9, 0xf9, 0x6a, 0x8f);
+ public static readonly Guid MF_MT_SUBTYPE =
+ new(0xf7e34c9a, 0x42e8, 0x4714, 0xb7, 0x4b, 0xcb, 0x29, 0xd7, 0x2c, 0x35, 0xe5);
+ public static readonly Guid MF_SOURCE_READER_ASYNC_CALLBACK =
+ new(0x1e3dbeac, 0xbb43, 0x4c35, 0xb5, 0x07, 0xcd, 0x64, 0x44, 0x64, 0xc9, 0x65);
+ public static readonly Guid MFMediaType_Video =
+ new(0x73646976, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_ARGB32 =
+ new(0x00000015, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_MJPG =
+ new(0x47504a4d, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_NV12 =
+ new(0x3231564e, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_RGB24 =
+ new(0x00000014, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_RGB32 =
+ new(0x00000016, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_RGB555 =
+ new(0x00000018, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_RGB565 =
+ new(0x00000017, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_UYVY =
+ new(0x59565955, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+ public static readonly Guid MFVideoFormat_YUY2 =
+ new(0x32595559, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+
+ public enum MF_SOURCE_READER_CONSTANTS
{
- [PreserveSig]
- int OnReadSample(int status, uint streamIndex, uint streamFlags, long timestamp, IntPtr sample);
+ MF_SOURCE_READER_INVALID_STREAM_INDEX = -1,
+ MF_SOURCE_READER_ALL_STREAMS = -2,
+ MF_SOURCE_READER_ANY_STREAM = -2,
+ MF_SOURCE_READER_FIRST_AUDIO_STREAM = -3,
+ MF_SOURCE_READER_FIRST_VIDEO_STREAM = -4,
+ MF_SOURCE_READER_MEDIASOURCE = -1,
+ }
+
+ [Flags]
+ public enum MF_SOURCE_READER_FLAG
+ {
+ MF_SOURCE_READERF_ERROR = 0x00000001,
+ MF_SOURCE_READERF_ENDOFSTREAM = 0x00000002,
+ MF_SOURCE_READERF_NEWSTREAM = 0x00000004,
+ MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED = 0x00000010,
+ MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED = 0x00000020,
+ MF_SOURCE_READERF_STREAMTICK = 0x00000100,
+ MF_SOURCE_READERF_ALLEFFECTSREMOVED = 0x00000200,
+ }
+
+ [Guid("00000000-0000-0000-C000-000000000046")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IUnknown
+ {
+ private void** lpVtbl;
+
+ public uint Release()
+ {
+ fixed (IUnknown* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[2])(self);
+ }
+ }
+ }
+
+ [Guid("2CD2D921-C447-44A7-A13C-4ADABFC247E3")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IMFAttributes
+ {
+ private void** lpVtbl;
+
+ public int GetUINT32(in Guid guidKey, out uint value)
+ {
+ fixed (IMFAttributes* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (uint* valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[7])(
+ self,
+ guidKeyLocal,
+ valueLocal);
+ }
+ }
+
+ public int GetUINT64(in Guid guidKey, out ulong value)
+ {
+ fixed (IMFAttributes* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (ulong* valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[8])(
+ self,
+ guidKeyLocal,
+ valueLocal);
+ }
+ }
+
+ public int GetGUID(in Guid guidKey, out Guid value)
+ {
+ fixed (IMFAttributes* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (Guid* valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[10])(
+ self,
+ guidKeyLocal,
+ valueLocal);
+ }
+ }
+
+ public int GetAllocatedString(in Guid guidKey, out IntPtr value, out uint length)
+ {
+ fixed (IMFAttributes* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (IntPtr* valueLocal = &value)
+ fixed (uint* lengthLocal = &length)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[13])(
+ self,
+ guidKeyLocal,
+ valueLocal,
+ lengthLocal);
+ }
+ }
- [PreserveSig]
- int OnFlush(uint streamIndex);
+ public int SetGUID(in Guid guidKey, in Guid value)
+ {
+ fixed (IMFAttributes* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (Guid* valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[24])(
+ self,
+ guidKeyLocal,
+ valueLocal);
+ }
+ }
- [PreserveSig]
- int OnEvent(uint streamIndex, IntPtr mediaEvent);
+ public int SetUnknown(in Guid guidKey, IUnknown* value)
+ {
+ fixed (IMFAttributes* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[27])(
+ self,
+ guidKeyLocal,
+ value);
+ }
+ }
}
+
+ [Guid("7FEE9E9A-4A89-47A6-899C-B6A53A70FB67")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IMFActivate
+ {
+ private void** lpVtbl;
+
+ public int GetAllocatedString(in Guid guidKey, out IntPtr value, out uint length)
+ {
+ fixed (IMFActivate* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (IntPtr* valueLocal = &value)
+ fixed (uint* lengthLocal = &length)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[13])(
+ self,
+ guidKeyLocal,
+ valueLocal,
+ lengthLocal);
+ }
+ }
+
+ public int ActivateObject(in Guid iid, out void* value)
+ {
+ fixed (IMFActivate* self = &this)
+ fixed (Guid* iidLocal = &iid)
+ fixed (void** valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[33])(
+ self,
+ iidLocal,
+ valueLocal);
+ }
+ }
+
+ public int ShutdownObject()
+ {
+ fixed (IMFActivate* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[34])(self);
+ }
+ }
+ }
+
+ [Guid("045FA593-8799-42B8-BC8D-8968C6453507")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IMFMediaBuffer
+ {
+ private void** lpVtbl;
+
+ public int Lock(out byte* data, out uint maximumLength, out uint currentLength)
+ {
+ fixed (IMFMediaBuffer* self = &this)
+ fixed (byte** dataLocal = &data)
+ fixed (uint* maximumLengthLocal = &maximumLength)
+ fixed (uint* currentLengthLocal = ¤tLength)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[3])(
+ self,
+ dataLocal,
+ maximumLengthLocal,
+ currentLengthLocal);
+ }
+ }
+
+ public int Unlock()
+ {
+ fixed (IMFMediaBuffer* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[4])(self);
+ }
+ }
+ }
+
+ [Guid("279A808D-AEC7-40C8-9C6B-A6B492C78A66")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IMFMediaSource
+ {
+ private void** lpVtbl;
+
+ public int Shutdown()
+ {
+ fixed (IMFMediaSource* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[12])(self);
+ }
+ }
+ }
+
+ [Guid("44AE0FA8-EA31-4109-8D2E-4CAE4997C555")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IMFMediaType
+ {
+ private void** lpVtbl;
+
+ public int GetUINT32(in Guid guidKey, out uint value)
+ {
+ fixed (IMFMediaType* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (uint* valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[7])(
+ self,
+ guidKeyLocal,
+ valueLocal);
+ }
+ }
+
+ public int GetUINT64(in Guid guidKey, out ulong value)
+ {
+ fixed (IMFMediaType* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (ulong* valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[8])(
+ self,
+ guidKeyLocal,
+ valueLocal);
+ }
+ }
+
+ public int GetGUID(in Guid guidKey, out Guid value)
+ {
+ fixed (IMFMediaType* self = &this)
+ fixed (Guid* guidKeyLocal = &guidKey)
+ fixed (Guid* valueLocal = &value)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[10])(
+ self,
+ guidKeyLocal,
+ valueLocal);
+ }
+ }
+ }
+
+ [Guid("C40A00F2-B93A-4D80-AE8C-5A1C634F58E4")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IMFSample
+ {
+ private void** lpVtbl;
+
+ public int ConvertToContiguousBuffer(out IMFMediaBuffer* buffer)
+ {
+ fixed (IMFSample* self = &this)
+ fixed (IMFMediaBuffer** bufferLocal = &buffer)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[41])(
+ self,
+ bufferLocal);
+ }
+ }
+ }
+
+ [Guid("70AE66F2-C809-4E4F-8915-BDCB406B7993")]
+ [StructLayout(LayoutKind.Sequential)]
+ public struct IMFSourceReader
+ {
+ private void** lpVtbl;
+
+ public int SetStreamSelection(uint streamIndex, bool selected)
+ {
+ fixed (IMFSourceReader* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[4])(
+ self,
+ streamIndex,
+ selected ? 1 : 0);
+ }
+ }
+
+ public int GetNativeMediaType(uint streamIndex, uint mediaTypeIndex, out IMFMediaType* mediaType)
+ {
+ fixed (IMFSourceReader* self = &this)
+ fixed (IMFMediaType** mediaTypeLocal = &mediaType)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[5])(
+ self,
+ streamIndex,
+ mediaTypeIndex,
+ mediaTypeLocal);
+ }
+ }
+
+ public int SetCurrentMediaType(uint streamIndex, IMFMediaType* mediaType)
+ {
+ fixed (IMFSourceReader* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[7])(
+ self,
+ streamIndex,
+ null,
+ mediaType);
+ }
+ }
+
+ public int ReadSample(
+ uint streamIndex,
+ uint controlFlags,
+ uint* actualStreamIndex,
+ uint* streamFlags,
+ long* timestamp,
+ IMFSample** sample)
+ {
+ fixed (IMFSourceReader* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[9])(
+ self,
+ streamIndex,
+ controlFlags,
+ actualStreamIndex,
+ streamFlags,
+ timestamp,
+ sample);
+ }
+ }
+
+ public int ReadSample(
+ uint streamIndex,
+ uint controlFlags,
+ out uint actualStreamIndex,
+ out uint streamFlags,
+ out long timestamp,
+ out IMFSample* sample)
+ {
+ fixed (uint* actualStreamIndexLocal = &actualStreamIndex)
+ fixed (uint* streamFlagsLocal = &streamFlags)
+ fixed (long* timestampLocal = ×tamp)
+ fixed (IMFSample** sampleLocal = &sample)
+ {
+ return this.ReadSample(
+ streamIndex,
+ controlFlags,
+ actualStreamIndexLocal,
+ streamFlagsLocal,
+ timestampLocal,
+ sampleLocal);
+ }
+ }
+
+ public int ReadSample(uint streamIndex, uint controlFlags) =>
+ this.ReadSample(streamIndex, controlFlags, null, null, null, null);
+
+ public int Flush(uint streamIndex)
+ {
+ fixed (IMFSourceReader* self = &this)
+ {
+ return ((delegate* unmanaged[Stdcall])this.lpVtbl[10])(
+ self,
+ streamIndex);
+ }
+ }
+ }
+
+#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+#endif
+ [DllImport("MFPlat.dll", ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
+ public static extern int MFCreateAttributes(IMFAttributes** attributes, uint initialSize);
+
+#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+#endif
+ [DllImport("MFPlat.dll", ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
+ public static extern int MFStartup(uint version, uint flags);
+
+#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+#endif
+ [DllImport("MFPlat.dll", ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
+ public static extern int MFShutdown();
+
+#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+#endif
+ [DllImport("MFReadWrite.dll", ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
+ public static extern int MFCreateSourceReaderFromMediaSource(
+ IMFMediaSource* mediaSource,
+ IMFAttributes* attributes,
+ IMFSourceReader** sourceReader);
+
+#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+#endif
+ [DllImport("MF.dll", ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
+ public static extern int MFEnumDeviceSources(
+ IMFAttributes* attributes,
+ out IMFActivate** devices,
+ out uint count);
+
public static IntPtr GetComInterfaceForObject(IMFSourceReaderCallbackInterop callback)
{
#if NET8_0_OR_GREATER
return ComWrappers.GetOrCreateComInterfaceForObject(callback, CreateComInterfaceFlags.None);
+#elif NETSTANDARD1_3
+ return Marshal.GetComInterfaceForObject<
+ IMFSourceReaderCallbackInterop,
+ IMFSourceReaderCallbackInterop>(callback);
#else
return Marshal.GetComInterfaceForObject(callback, typeof(IMFSourceReaderCallbackInterop));
#endif
}
- public static void ReleaseComInterface(IntPtr pointer) => _ = Marshal.Release(pointer);
+ public static void ReleaseComInterface(IntPtr pointer)
+ {
+ if (pointer != IntPtr.Zero)
+ {
+ _ = ((IUnknown*)pointer)->Release();
+ }
+ }
+
+ public static void Release(T* value)
+ where T : unmanaged
+ {
+ if (value is not null)
+ {
+ _ = ((IUnknown*)value)->Release();
+ }
+ }
#if NET8_0_OR_GREATER
private static readonly StrategyBasedComWrappers ComWrappers = new();
#endif
}
-#endif
\ No newline at end of file
+
+// The runtime COM marshaler requires this callback interface to be publicly visible.
+// Keep the inbound raw COM definitions in the internal NativeMethods container.
+///
+/// Defines callbacks from an asynchronous Media Foundation Source Reader.
+///
+///
+/// This interface is public because the runtime COM marshaler requires public visibility
+/// when it creates the COM callable wrapper used by target frameworks earlier than .NET 8.
+///
+#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);
+}
+#endif
diff --git a/FlashCap.Core/NativeMethods.json b/FlashCap.Core/NativeMethods.json
deleted file mode 100644
index 41081a0..0000000
--- a/FlashCap.Core/NativeMethods.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "$schema": "https://aka.ms/CsWin32.schema.json",
- "allowMarshaling": false,
- "friendlyOverloads": {
- "enabled": true,
- "comOutPtrGenericOverloads": false
- },
- "useSafeHandles": false,
- "comInterop": {
- "preserveSigMethods": [ "*" ]
- }
-}
diff --git a/FlashCap.Core/NativeMethods.txt b/FlashCap.Core/NativeMethods.txt
deleted file mode 100644
index 358cbe4..0000000
--- a/FlashCap.Core/NativeMethods.txt
+++ /dev/null
@@ -1,40 +0,0 @@
-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_ASYNC_CALLBACK
-MF_SOURCE_READER_FLAG
-MFMediaType_Video
-MFVideoFormat_ARGB32
-MFVideoFormat_MJPG
-MFVideoFormat_NV12
-MFVideoFormat_RGB24
-MFVideoFormat_RGB32
-MFVideoFormat_RGB555
-MFVideoFormat_RGB565
-MFVideoFormat_UYVY
-MFVideoFormat_YUY2
diff --git a/FlashCap.sln b/FlashCap.sln
index 1cc1b32..f0bb01d 100644
--- a/FlashCap.sln
+++ b/FlashCap.sln
@@ -31,12 +31,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.Wpf", "samples\Fla
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.Core", "FlashCap.Core\FlashCap.Core.csproj", "{8D251497-C11E-4FE6-9EFF-913FBA826C6D}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.Core.Tests", "FlashCap.Core.Tests\FlashCap.Core.Tests.csproj", "{74C3DDF5-7599-45CE-8AA8-B1EBC55BF171}"
+EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.FlashCap", "FSharp.FlashCap\FSharp.FlashCap.fsproj", "{AAD7F2E9-C6F7-4BDA-9D49-08469C2C7A1D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlashCap.OneShot", "samples\FlashCap.OneShot\FlashCap.OneShot.csproj", "{2914E44E-F6F4-4BF0-B0BB-962DE4D505AE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlashCap.Avalonia.UI", "samples\FlashCap.Avalonia\FlashCap.Avalonia.UI\FlashCap.Avalonia.UI.csproj", "{539C6CDD-B165-4233-9F83-DF61F4A735AA}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlashCap.Core.LegacyInteropSmoke", "tests\FlashCap.Core.LegacyInteropSmoke\FlashCap.Core.LegacyInteropSmoke.csproj", "{06150000-B9CA-4BDF-A2AE-7EE2FC8D1588}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -67,6 +73,10 @@ Global
{8D251497-C11E-4FE6-9EFF-913FBA826C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D251497-C11E-4FE6-9EFF-913FBA826C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D251497-C11E-4FE6-9EFF-913FBA826C6D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {74C3DDF5-7599-45CE-8AA8-B1EBC55BF171}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {74C3DDF5-7599-45CE-8AA8-B1EBC55BF171}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {74C3DDF5-7599-45CE-8AA8-B1EBC55BF171}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {74C3DDF5-7599-45CE-8AA8-B1EBC55BF171}.Release|Any CPU.Build.0 = Release|Any CPU
{AAD7F2E9-C6F7-4BDA-9D49-08469C2C7A1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AAD7F2E9-C6F7-4BDA-9D49-08469C2C7A1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAD7F2E9-C6F7-4BDA-9D49-08469C2C7A1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -79,6 +89,10 @@ Global
{539C6CDD-B165-4233-9F83-DF61F4A735AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{539C6CDD-B165-4233-9F83-DF61F4A735AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{539C6CDD-B165-4233-9F83-DF61F4A735AA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {06150000-B9CA-4BDF-A2AE-7EE2FC8D1588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {06150000-B9CA-4BDF-A2AE-7EE2FC8D1588}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {06150000-B9CA-4BDF-A2AE-7EE2FC8D1588}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {06150000-B9CA-4BDF-A2AE-7EE2FC8D1588}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -90,6 +104,7 @@ Global
{583903DC-9D94-4A63-AFCB-2FB88CE14C95} = {655486CC-57A5-494E-97D0-9850B6D55BBA}
{2914E44E-F6F4-4BF0-B0BB-962DE4D505AE} = {655486CC-57A5-494E-97D0-9850B6D55BBA}
{539C6CDD-B165-4233-9F83-DF61F4A735AA} = {A1BDFB2A-48C0-4D65-AE03-4DBFC12B4BA8}
+ {06150000-B9CA-4BDF-A2AE-7EE2FC8D1588} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {53E3C2D2-05FD-4396-ABF1-6B313F8F82A9}
diff --git a/FlashCap/CaptureDevicesExtension.cs b/FlashCap/CaptureDevicesExtension.cs
index f70a69e..def661b 100644
--- a/FlashCap/CaptureDevicesExtension.cs
+++ b/FlashCap/CaptureDevicesExtension.cs
@@ -8,17 +8,41 @@
////////////////////////////////////////////////////////////////////////////
using System.Collections.Generic;
+#if NET6_0_OR_GREATER
+using System.Diagnostics.CodeAnalysis;
+#endif
using System.Linq;
+using FlashCap.Devices;
namespace FlashCap;
public static class CaptureDevicesExtension
{
+#if NET6_0_OR_GREATER
+ [RequiresUnreferencedCode("Default device enumeration includes DirectShow, which requires runtime-generated COM interop. Use a platform-specific Devices type for Native AOT.")]
+#endif
public static IEnumerable EnumerateDescriptors(
this CaptureDevices captureDevices) =>
captureDevices.InternalEnumerateDescriptors();
+#if NET6_0_OR_GREATER
+ [RequiresUnreferencedCode("Default device enumeration includes DirectShow, which requires runtime-generated COM interop. Use a platform-specific Devices type for Native AOT.")]
+#endif
public static CaptureDeviceDescriptor[] GetDescriptors(
this CaptureDevices captureDevices) =>
captureDevices.InternalEnumerateDescriptors().ToArray();
+
+#if NET6_0_OR_GREATER
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The Media Foundation override does not require unreferenced code.")]
+#endif
+ public static IEnumerable EnumerateDescriptors(
+ this MediaFoundationDevices captureDevices) =>
+ captureDevices.InternalEnumerateDescriptors();
+
+#if NET6_0_OR_GREATER
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The Media Foundation override does not require unreferenced code.")]
+#endif
+ public static CaptureDeviceDescriptor[] GetDescriptors(
+ this MediaFoundationDevices captureDevices) =>
+ captureDevices.InternalEnumerateDescriptors().ToArray();
}
diff --git a/README.md b/README.md
index af3c41b..c562168 100644
--- a/README.md
+++ b/README.md
@@ -131,7 +131,7 @@ Published introduction article: ["Easy to implement video image capture with Fla
.NET platforms supported are as follows (almost all!):
-* .NET 9 to 5 (`net9.0` and etc)
+* .NET 10 to 5 (`net10.0` and etc)
* .NET Core 3.1, 3.0, 2.2, 2.1, 2.0 (`netcoreapp3.1` and etc)
* .NET Standard 2.1, 2.0, 1.3 (`netstandard2.1` and etc)
* .NET Framework 4.8, 4.6.1, 4.5, 4.0, 3.5 (`net48` and etc)
@@ -140,6 +140,7 @@ Platforms on which capture devices can be used:
* Windows (DirectShow devices, tested on x64/x86)
* Windows (Video for Windows devices, tested on x64/x86)
+* Windows 7 or later (Media Foundation devices, available from all 19 supported TFMs listed above, including `net35`)
* Linux (V4L2 devices, supported on x86_64/i686/aarch64/armv7l/mips)
* OSX (AVFoundation devices, supported on x86_64/arm64)
@@ -595,6 +596,12 @@ var devices = new CaptureDevices(bufferPool);
It is used as a common buffer pooling for all devices enumerated from this instance.
+## Native AOT (Advanced topic)
+
+On `net8.0` and later, its COM callback uses source-generated COM interop, making this backend compatible with Native AOT; earlier TFMs use the runtime's built-in COM interop to expose the callback as a COM callable wrapper (CCW).
+
+DirectShow continues to rely on runtime-generated COM interop and is not compatible with Native AOT, so Native AOT applications should use `MediaFoundationDevices` directly instead of the default `CaptureDevices` enumeration that also includes DirectShow.
+
## Master for frame processor (Advanced topic)
Welcome to the underground dungeon, where FlashCap's frame processor is a polished gem.
diff --git a/README_ja.md b/README_ja.md
index 637058f..eafc19f 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -132,7 +132,7 @@ await deviceObservable.StartAsync();
対応する.NETプラットフォームは以下の通りです(ほぼ全てです!):
-* .NET 9 to 5 (`net9.0` and etc)
+* .NET 10 to 5 (`net10.0` and etc)
* .NET Core 3.1, 3.0, 2.2, 2.1, 2.0 (`netcoreapp3.1` and etc)
* .NET Standard 2.1, 2.0, 1.3 (`netstandard2.1` and etc)
* .NET Framework 4.8, 4.6.1, 4.5, 4.0, 3.5 (`net48` and etc)
@@ -141,6 +141,7 @@ await deviceObservable.StartAsync();
* Windows (DirectShowデバイス, x64/x86)
* Windows (Video for Windowsデバイス, x64/x86)
+* Windows 7以降 (Media Foundationデバイス, `net35`を含む上記19個全ての対応TFMから利用可能)
* Linux (V4L2デバイス, x86_64/i686/aarch64/armv7l/mips)
* OSX (AVFoundationデバイス, x86_64/arm64)
@@ -549,6 +550,12 @@ var devices = new CaptureDevices(bufferPool);
この`CaptureDevices`のインスタンスから列挙された全てのデバイスで、共通のバッファプーリングとして使用されます。
+## Native AOT (Advanced topic)
+
+`net8.0`以降では、COMコールバックにソース生成COM相互運用を使用するため、このバックエンドはNative AOTに対応します。それ以前のTFMでは、ランタイムの組み込みCOM相互運用を使用して、コールバックをCOM Callable Wrapper (CCW)として公開します。
+
+DirectShowは引き続きランタイム生成COM相互運用に依存し、Native AOTには対応しません。そのため、Native AOTアプリケーションでは、DirectShowも含む既定の`CaptureDevices`列挙ではなく、`MediaFoundationDevices`を直接使用して下さい。
+
## フレームプロセッサをマスターする (Advanced topic)
地下ダンジョンへようこそ。FlashCapのフレームプロセッサは、磨けば光る宝石です。しかし、余程のことが無い限り、フレームプロセッサを理解する必要はありません。この解説は、やむを得ずフレームプロセッサを扱う場合の参考にして下さい。また、FlashCapが[デフォルトで内蔵するフレームプロセッサの実装](https://github.com/kekyo/FlashCap/tree/main/FlashCap/FrameProcessors)も参考になるでしょう。
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..3140116
--- /dev/null
+++ b/global.json
@@ -0,0 +1,5 @@
+{
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/scripts/test-all.mjs b/scripts/test-all.mjs
new file mode 100644
index 0000000..27ae5e7
--- /dev/null
+++ b/scripts/test-all.mjs
@@ -0,0 +1,306 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// FlashCap - Independent camera capture library.
+// Copyright (c) Kouji Matsui (@kekyo@mi.kekyo.net)
+//
+// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
+//
+////////////////////////////////////////////////////////////////////////////
+
+import { spawnSync } from "node:child_process";
+import {
+ copyFileSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ readdirSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const temporaryRoot = mkdtempSync(join(tmpdir(), "flashcap-tests-"));
+const packageDirectory = join(temporaryRoot, "packages");
+const packageCache = join(temporaryRoot, "nuget-packages");
+const httpCache = join(temporaryRoot, "nuget-http-cache");
+const consumerNuGetConfig = join(temporaryRoot, "consumer.NuGet.Config");
+const consumerSourceDirectory = join(
+ repositoryRoot,
+ "tests",
+ "FlashCap.PackageConsumer",
+);
+const consumerDirectory = join(temporaryRoot, "consumer");
+const consumerProject = join(consumerDirectory, "FlashCap.PackageConsumer.csproj");
+const verifierProject = join(
+ repositoryRoot,
+ "tests",
+ "FlashCap.PackageVerifier",
+ "FlashCap.PackageVerifier.csproj",
+);
+const aotSourceDirectory = join(
+ repositoryRoot,
+ "tests",
+ "FlashCap.Core.AotSmoke",
+);
+const aotDirectory = join(temporaryRoot, "aot-source");
+const aotProject = join(aotDirectory, "FlashCap.Core.AotSmoke.csproj");
+const legacyInteropDirectory = join(
+ repositoryRoot,
+ "tests",
+ "FlashCap.Core.LegacyInteropSmoke",
+);
+const isolatedEnvironment = {
+ ...process.env,
+ DOTNET_CLI_TELEMETRY_OPTOUT: "1",
+ NUGET_PACKAGES: packageCache,
+ NUGET_HTTP_CACHE_PATH: httpCache,
+};
+const forbiddenPackageIds = new Set([
+ "microsoft.windows.cswin32",
+ "system.memory",
+ "system.runtime.compilerservices.unsafe",
+]);
+
+const run = (command, args, environment = process.env) => {
+ process.stdout.write(`\n> ${command} ${args.join(" ")}\n`);
+ const result = spawnSync(command, args, {
+ cwd: repositoryRoot,
+ env: environment,
+ stdio: "inherit",
+ });
+ if (result.error) {
+ throw result.error;
+ }
+ if (result.status !== 0) {
+ throw new Error(`${command} failed with exit code ${result.status}.`);
+ }
+};
+
+const findCorePackage = () => {
+ const candidates = readdirSync(packageDirectory).filter(
+ (name) => /^FlashCap\.Core\..+\.nupkg$/.test(name) && !name.endsWith(".snupkg"),
+ );
+ if (candidates.length !== 1) {
+ throw new Error(
+ `Expected one FlashCap.Core package, found ${candidates.length}.`,
+ );
+ }
+ const fileName = candidates[0];
+ const version = fileName.slice("FlashCap.Core.".length, -".nupkg".length);
+ return { fileName, version };
+};
+
+const verifyCoreRestoreGraph = () => {
+ const assetsPath = join(
+ repositoryRoot,
+ "FlashCap.Core",
+ "obj",
+ "project.assets.json",
+ );
+ const assets = JSON.parse(readFileSync(assetsPath, "utf8"));
+ const failures = [];
+ for (const [targetName, target] of Object.entries(assets.targets ?? {})) {
+ for (const libraryName of Object.keys(target)) {
+ const separator = libraryName.lastIndexOf("/");
+ const packageId = (
+ separator < 0 ? libraryName : libraryName.slice(0, separator)
+ ).toLowerCase();
+ if (forbiddenPackageIds.has(packageId)) {
+ failures.push(`${targetName}: ${packageId}`);
+ }
+ }
+ }
+ if (failures.length !== 0) {
+ throw new Error(
+ `FlashCap.Core restore graph contains forbidden packages:\n${failures.join("\n")}`,
+ );
+ }
+};
+
+const writeConsumerNuGetConfig = () => {
+ const generatedSource = packageDirectory
+ .replaceAll("&", "&")
+ .replaceAll('"', """)
+ .replaceAll("<", "<")
+ .replaceAll(">", ">");
+ writeFileSync(
+ consumerNuGetConfig,
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`,
+ "utf8",
+ );
+};
+
+try {
+ mkdirSync(consumerDirectory);
+ for (const fileName of ["FlashCap.PackageConsumer.csproj", "PackageSurface.cs"]) {
+ copyFileSync(
+ join(consumerSourceDirectory, fileName),
+ join(consumerDirectory, fileName),
+ );
+ }
+ mkdirSync(aotDirectory);
+ for (const fileName of ["FlashCap.Core.AotSmoke.csproj", "Program.cs"]) {
+ copyFileSync(
+ join(aotSourceDirectory, fileName),
+ join(aotDirectory, fileName),
+ );
+ }
+
+ run("dotnet", ["restore", "FlashCap.sln", "--force", "--no-cache"]);
+ verifyCoreRestoreGraph();
+ run("dotnet", [
+ "build",
+ "FlashCap.sln",
+ "--configuration",
+ "Release",
+ "--no-restore",
+ ]);
+ run("dotnet", [
+ "test",
+ "--solution",
+ "FlashCap.sln",
+ "--configuration",
+ "Release",
+ "--no-build",
+ "--no-restore",
+ "--minimum-expected-tests",
+ "134",
+ ]);
+ for (const framework of ["net35", "net40", "net45", "net461", "net48"]) {
+ run("mono", [
+ join(
+ legacyInteropDirectory,
+ "bin",
+ "Release",
+ framework,
+ "FlashCap.Core.Tests.exe",
+ ),
+ ]);
+ }
+ run("dotnet", [
+ "pack",
+ "FlashCap.sln",
+ "--configuration",
+ "Release",
+ "--no-build",
+ "--no-restore",
+ "--output",
+ packageDirectory,
+ ]);
+
+ const corePackage = findCorePackage();
+ writeConsumerNuGetConfig();
+ run(
+ "dotnet",
+ [
+ "restore",
+ consumerProject,
+ "--force",
+ "--no-cache",
+ "--configfile",
+ consumerNuGetConfig,
+ `-p:FlashCapPackageVersion=${corePackage.version}`,
+ ],
+ isolatedEnvironment,
+ );
+ run(
+ "dotnet",
+ [
+ "build",
+ consumerProject,
+ "--configuration",
+ "Release",
+ "--no-restore",
+ `-p:FlashCapPackageVersion=${corePackage.version}`,
+ ],
+ isolatedEnvironment,
+ );
+ for (const framework of ["net35", "net40", "net45", "net461", "net48"]) {
+ run(
+ "mono",
+ [
+ join(
+ dirname(consumerProject),
+ "bin",
+ "Release",
+ framework,
+ "FlashCap.PackageConsumer.exe",
+ ),
+ ],
+ isolatedEnvironment,
+ );
+ }
+ run("dotnet", [
+ "run",
+ "--project",
+ verifierProject,
+ "--configuration",
+ "Release",
+ "--",
+ join(packageDirectory, corePackage.fileName),
+ join(dirname(consumerProject), "obj", "project.assets.json"),
+ ]);
+
+ run(
+ "dotnet",
+ [
+ "restore",
+ aotProject,
+ "--force",
+ "--no-cache",
+ "--runtime",
+ "linux-x64",
+ "--configfile",
+ consumerNuGetConfig,
+ `-p:FlashCapPackageVersion=${corePackage.version}`,
+ ],
+ isolatedEnvironment,
+ );
+
+ for (const framework of ["net8.0", "net10.0"]) {
+ const outputDirectory = join(temporaryRoot, "aot", framework);
+ run("dotnet", [
+ "publish",
+ aotProject,
+ "--configuration",
+ "Release",
+ "--framework",
+ framework,
+ "--runtime",
+ "linux-x64",
+ "--self-contained",
+ "true",
+ "--no-restore",
+ "-p:PublishAot=true",
+ "-p:StripSymbols=true",
+ `-p:FlashCapPackageVersion=${corePackage.version}`,
+ "--output",
+ outputDirectory,
+ ], isolatedEnvironment);
+ run(join(outputDirectory, "FlashCap.Core.Tests"), []);
+ }
+
+ process.stdout.write("\nAll FlashCap verification steps passed.\n");
+} finally {
+ rmSync(temporaryRoot, { recursive: true, force: true });
+}
diff --git a/tests/FlashCap.Core.AotSmoke/FlashCap.Core.AotSmoke.csproj b/tests/FlashCap.Core.AotSmoke/FlashCap.Core.AotSmoke.csproj
new file mode 100644
index 0000000..f15e8d2
--- /dev/null
+++ b/tests/FlashCap.Core.AotSmoke/FlashCap.Core.AotSmoke.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Exe
+ net8.0;net10.0
+ FlashCap.Core.Tests
+ FlashCap.Core.AotSmoke
+ true
+ true
+ enable
+ true
+ $(WarningsAsErrors);IL2026;IL2104
+ false
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/FlashCap.Core.AotSmoke/Program.cs b/tests/FlashCap.Core.AotSmoke/Program.cs
new file mode 100644
index 0000000..eb181f9
--- /dev/null
+++ b/tests/FlashCap.Core.AotSmoke/Program.cs
@@ -0,0 +1,164 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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.Marshalling;
+using FlashCap;
+using FlashCap.Devices;
+using FlashCap.Internal;
+
+namespace FlashCap.Core.AotSmoke;
+
+internal static partial class Program
+{
+ private static readonly Guid CallbackInterfaceId =
+ new("DEEC8D99-FA1D-4D82-84C2-2C8969944867");
+
+ private static int Main()
+ {
+ try
+ {
+#pragma warning disable CA1416
+ _ = new MediaFoundationDevices().GetDescriptors();
+#pragma warning restore CA1416
+ Console.Error.WriteLine("Media Foundation enumeration unexpectedly ran on Linux.");
+ return 1;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ }
+
+ var callback = new SourceReaderCallback();
+ IntPtr callbackPointer = IntPtr.Zero;
+ IntPtr callbackInterface = IntPtr.Zero;
+ try
+ {
+ // The smoke test intentionally exercises the platform-independent generated COM path on Linux.
+#pragma warning disable CA1416
+ callbackPointer = NativeMethods_MediaFoundation.GetComInterfaceForObject(
+ callback);
+#pragma warning restore CA1416
+ if (callbackPointer == IntPtr.Zero)
+ {
+ Console.Error.WriteLine("The generated COM interface pointer was null.");
+ return 2;
+ }
+
+ unsafe
+ {
+ var unknownVtable = *(void***)callbackPointer;
+ var queryInterface =
+ (delegate* unmanaged[Stdcall])unknownVtable[0];
+ var callbackInterfaceId = CallbackInterfaceId;
+ if (queryInterface(
+ callbackPointer,
+ &callbackInterfaceId,
+ &callbackInterface) < 0 ||
+ callbackInterface == IntPtr.Zero)
+ {
+ return 3;
+ }
+
+ var vtable = *(void***)callbackInterface;
+ var onReadSample =
+ (delegate* unmanaged[Stdcall])vtable[3];
+ var onFlush = (delegate* unmanaged[Stdcall])vtable[4];
+ var onEvent = (delegate* unmanaged[Stdcall])vtable[5];
+ if (onReadSample(callbackInterface, -7, 11, 13, 17, new IntPtr(19)) != 0)
+ {
+ return 3;
+ }
+ if (onFlush(callbackInterface, 23) != 0)
+ {
+ return 3;
+ }
+ if (onEvent(callbackInterface, 29, new IntPtr(31)) != 0)
+ {
+ return 3;
+ }
+ }
+ if (callback.Status != -7 ||
+ callback.ReadStreamIndex != 11 ||
+ callback.StreamFlags != 13 ||
+ callback.Timestamp != 17 ||
+ callback.Sample != new IntPtr(19) ||
+ callback.FlushStreamIndex != 23 ||
+ callback.EventStreamIndex != 29 ||
+ callback.MediaEvent != new IntPtr(31))
+ {
+ return 4;
+ }
+
+ return 0;
+ }
+ catch (Exception exception)
+ {
+ Console.Error.WriteLine(exception);
+ return 5;
+ }
+ finally
+ {
+ if (callbackInterface != IntPtr.Zero)
+ {
+#pragma warning disable CA1416
+ NativeMethods_MediaFoundation.ReleaseComInterface(callbackInterface);
+#pragma warning restore CA1416
+ }
+ if (callbackPointer != IntPtr.Zero)
+ {
+ // The smoke test intentionally exercises the platform-independent raw IUnknown release path on Linux.
+#pragma warning disable CA1416
+ NativeMethods_MediaFoundation.ReleaseComInterface(callbackPointer);
+#pragma warning restore CA1416
+ }
+ }
+ }
+
+ [GeneratedComClass]
+ private sealed partial class SourceReaderCallback :
+ IMFSourceReaderCallbackInterop
+ {
+ internal int Status;
+ internal uint ReadStreamIndex;
+ internal uint StreamFlags;
+ internal long Timestamp;
+ internal IntPtr Sample;
+ internal uint FlushStreamIndex;
+ internal uint EventStreamIndex;
+ internal IntPtr MediaEvent;
+
+ public int OnReadSample(
+ int status,
+ uint streamIndex,
+ uint streamFlags,
+ long timestamp,
+ IntPtr sample)
+ {
+ this.Status = status;
+ this.ReadStreamIndex = streamIndex;
+ this.StreamFlags = streamFlags;
+ this.Timestamp = timestamp;
+ this.Sample = sample;
+ return 0;
+ }
+
+ public int OnFlush(uint streamIndex)
+ {
+ this.FlushStreamIndex = streamIndex;
+ return 0;
+ }
+
+ public int OnEvent(uint streamIndex, IntPtr mediaEvent)
+ {
+ this.EventStreamIndex = streamIndex;
+ this.MediaEvent = mediaEvent;
+ return 0;
+ }
+ }
+}
diff --git a/tests/FlashCap.Core.LegacyInteropSmoke/FlashCap.Core.LegacyInteropSmoke.csproj b/tests/FlashCap.Core.LegacyInteropSmoke/FlashCap.Core.LegacyInteropSmoke.csproj
new file mode 100644
index 0000000..155865e
--- /dev/null
+++ b/tests/FlashCap.Core.LegacyInteropSmoke/FlashCap.Core.LegacyInteropSmoke.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net35;net40;net45;net461;net48
+ FlashCap.Core.Tests
+ FlashCap.Core.LegacyInteropSmoke
+ true
+ false
+
+
+
+
+
+
+
diff --git a/tests/FlashCap.Core.LegacyInteropSmoke/Program.cs b/tests/FlashCap.Core.LegacyInteropSmoke/Program.cs
new file mode 100644
index 0000000..aa6c66c
--- /dev/null
+++ b/tests/FlashCap.Core.LegacyInteropSmoke/Program.cs
@@ -0,0 +1,132 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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 FlashCap.Internal;
+
+namespace FlashCap.Core.LegacyInteropSmoke;
+
+internal static unsafe class Program
+{
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ private delegate int OnReadSampleDelegate(
+ IntPtr self,
+ int status,
+ uint streamIndex,
+ uint streamFlags,
+ long timestamp,
+ IntPtr sample);
+
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ private delegate int OnFlushDelegate(IntPtr self, uint streamIndex);
+
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ private delegate int OnEventDelegate(IntPtr self, uint streamIndex, IntPtr mediaEvent);
+
+ private static int Main()
+ {
+ var callback = new SourceReaderCallback();
+ var callbackPointer = IntPtr.Zero;
+ try
+ {
+ if (!typeof(IMFSourceReaderCallbackInterop).IsVisible)
+ {
+ Console.Error.WriteLine("The callback interface is not visible to COM.");
+ return 5;
+ }
+
+ callbackPointer = NativeMethods_MediaFoundation.GetComInterfaceForObject(callback);
+ if (callbackPointer == IntPtr.Zero)
+ {
+ return 1;
+ }
+
+ var vtable = *(IntPtr**)callbackPointer;
+ var onReadSample = (OnReadSampleDelegate)Marshal.GetDelegateForFunctionPointer(
+ vtable[3], typeof(OnReadSampleDelegate));
+ var onFlush = (OnFlushDelegate)Marshal.GetDelegateForFunctionPointer(
+ vtable[4], typeof(OnFlushDelegate));
+ var onEvent = (OnEventDelegate)Marshal.GetDelegateForFunctionPointer(
+ vtable[5], typeof(OnEventDelegate));
+
+ if (onReadSample(callbackPointer, -7, 11, 13, 17, new IntPtr(19)) != 0 ||
+ onFlush(callbackPointer, 23) != 0 ||
+ onEvent(callbackPointer, 29, new IntPtr(31)) != 0)
+ {
+ return 2;
+ }
+ if (callback.Status != -7 ||
+ callback.ReadStreamIndex != 11 ||
+ callback.StreamFlags != 13 ||
+ callback.Timestamp != 17 ||
+ callback.Sample != new IntPtr(19) ||
+ callback.FlushStreamIndex != 23 ||
+ callback.EventStreamIndex != 29 ||
+ callback.MediaEvent != new IntPtr(31))
+ {
+ return 3;
+ }
+
+ return 0;
+ }
+ catch (Exception exception)
+ {
+ Console.Error.WriteLine(exception);
+ return 4;
+ }
+ finally
+ {
+ NativeMethods_MediaFoundation.ReleaseComInterface(callbackPointer);
+ }
+ }
+
+ [ComVisible(true)]
+ [ClassInterface(ClassInterfaceType.None)]
+ private sealed class SourceReaderCallback :
+ IMFSourceReaderCallbackInterop
+ {
+ internal int Status;
+ internal uint ReadStreamIndex;
+ internal uint StreamFlags;
+ internal long Timestamp;
+ internal IntPtr Sample;
+ internal uint FlushStreamIndex;
+ internal uint EventStreamIndex;
+ internal IntPtr MediaEvent;
+
+ public int OnReadSample(
+ int status,
+ uint streamIndex,
+ uint streamFlags,
+ long timestamp,
+ IntPtr sample)
+ {
+ this.Status = status;
+ this.ReadStreamIndex = streamIndex;
+ this.StreamFlags = streamFlags;
+ this.Timestamp = timestamp;
+ this.Sample = sample;
+ return 0;
+ }
+
+ public int OnFlush(uint streamIndex)
+ {
+ this.FlushStreamIndex = streamIndex;
+ return 0;
+ }
+
+ public int OnEvent(uint streamIndex, IntPtr mediaEvent)
+ {
+ this.EventStreamIndex = streamIndex;
+ this.MediaEvent = mediaEvent;
+ return 0;
+ }
+ }
+}
diff --git a/tests/FlashCap.PackageConsumer/FlashCap.PackageConsumer.csproj b/tests/FlashCap.PackageConsumer/FlashCap.PackageConsumer.csproj
new file mode 100644
index 0000000..93efe52
--- /dev/null
+++ b/tests/FlashCap.PackageConsumer/FlashCap.PackageConsumer.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net35;net40;net45;net461;net48;netstandard1.3;netstandard2.0;netstandard2.1;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0
+ 7.3
+ disable
+ false
+ false
+ $(NoWarn);NETSDK1215
+ false
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/FlashCap.PackageConsumer/PackageSurface.cs b/tests/FlashCap.PackageConsumer/PackageSurface.cs
new file mode 100644
index 0000000..ff03727
--- /dev/null
+++ b/tests/FlashCap.PackageConsumer/PackageSurface.cs
@@ -0,0 +1,32 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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.Devices;
+using System;
+
+namespace FlashCap.PackageConsumer
+{
+ public static class PackageSurface
+ {
+ public static readonly Type[] MediaFoundationTypes =
+ {
+ typeof(MediaFoundationDevices),
+ typeof(MediaFoundationDeviceDescriptor),
+ typeof(MediaFoundationDevice),
+ };
+
+#if NETFRAMEWORK
+ public static int Main()
+ {
+ _ = new MediaFoundationDevices();
+ return MediaFoundationTypes.Length == 3 ? 0 : 1;
+ }
+#endif
+ }
+}
diff --git a/tests/FlashCap.PackageVerifier/FlashCap.PackageVerifier.csproj b/tests/FlashCap.PackageVerifier/FlashCap.PackageVerifier.csproj
new file mode 100644
index 0000000..dfb40ca
--- /dev/null
+++ b/tests/FlashCap.PackageVerifier/FlashCap.PackageVerifier.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/tests/FlashCap.PackageVerifier/Program.cs b/tests/FlashCap.PackageVerifier/Program.cs
new file mode 100644
index 0000000..5633d75
--- /dev/null
+++ b/tests/FlashCap.PackageVerifier/Program.cs
@@ -0,0 +1,385 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// 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.IO.Compression;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using System.Text.Json;
+using System.Xml.Linq;
+
+namespace FlashCap.PackageVerifier;
+
+internal static class Program
+{
+ private static readonly string[] ExpectedFrameworks =
+ [
+ "net35",
+ "net40",
+ "net45",
+ "net461",
+ "net48",
+ "netstandard1.3",
+ "netstandard2.0",
+ "netstandard2.1",
+ "netcoreapp2.0",
+ "netcoreapp2.1",
+ "netcoreapp2.2",
+ "netcoreapp3.0",
+ "netcoreapp3.1",
+ "net5.0",
+ "net6.0",
+ "net7.0",
+ "net8.0",
+ "net9.0",
+ "net10.0",
+ ];
+
+ private static readonly HashSet LegacyMemoryFrameworks =
+ new(StringComparer.OrdinalIgnoreCase)
+ {
+ "net35",
+ "net40",
+ "net45",
+ "net461",
+ "net48",
+ "netstandard1.3",
+ "netstandard2.0",
+ "netstandard2.1",
+ "netcoreapp2.0",
+ "netcoreapp2.1",
+ "netcoreapp2.2",
+ };
+
+ private static readonly HashSet ModernMarshallingFrameworks =
+ new(StringComparer.OrdinalIgnoreCase)
+ {
+ "net8.0",
+ "net9.0",
+ "net10.0",
+ };
+
+ private static readonly HashSet ForbiddenPackageIds =
+ new(StringComparer.OrdinalIgnoreCase)
+ {
+ "Microsoft.Windows.CsWin32",
+ "System.Memory",
+ "System.Runtime.CompilerServices.Unsafe",
+ };
+
+ private static readonly string[] RequiredPublicTypes =
+ [
+ "FlashCap.Devices.MediaFoundationDevices",
+ "FlashCap.Devices.MediaFoundationDeviceDescriptor",
+ "FlashCap.Devices.MediaFoundationDevice",
+ "FlashCap.Internal.IMFSourceReaderCallbackInterop",
+ ];
+
+ private readonly record struct PackageIdentity(string Version);
+
+ private static int Main(string[] args)
+ {
+ if (args.Length != 2)
+ {
+ Console.Error.WriteLine(
+ "Usage: FlashCap.PackageVerifier ");
+ return 2;
+ }
+
+ try
+ {
+ var failures = new List();
+ var package = VerifyPackage(Path.GetFullPath(args[0]), failures);
+ VerifyConsumerAssets(Path.GetFullPath(args[1]), package, failures);
+
+ if (failures.Count != 0)
+ {
+ foreach (var failure in failures)
+ {
+ Console.Error.WriteLine($"ERROR: {failure}");
+ }
+ Console.Error.WriteLine($"Package verification failed with {failures.Count} violation(s).");
+ return 1;
+ }
+
+ Console.WriteLine(
+ $"Verified {ExpectedFrameworks.Length} FlashCap.Core package assets and all consumer targets.");
+ return 0;
+ }
+ catch (Exception exception)
+ {
+ Console.Error.WriteLine($"ERROR: Package verification could not be completed: {exception.Message}");
+ return 2;
+ }
+ }
+
+ private static PackageIdentity VerifyPackage(string packagePath, List failures)
+ {
+ if (!File.Exists(packagePath))
+ {
+ throw new FileNotFoundException("The package does not exist.", packagePath);
+ }
+
+ using var stream = File.OpenRead(packagePath);
+ using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
+
+ var version = VerifyNuspec(archive, failures);
+
+ var assemblyEntries = archive.Entries
+ .Where(entry =>
+ entry.FullName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase) &&
+ entry.FullName.EndsWith("/FlashCap.Core.dll", StringComparison.OrdinalIgnoreCase))
+ .GroupBy(GetFrameworkFromLibraryPath, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.OrdinalIgnoreCase);
+
+ foreach (var framework in ExpectedFrameworks)
+ {
+ if (!assemblyEntries.TryGetValue(framework, out var entries))
+ {
+ failures.Add($"The package is missing lib/{framework}/FlashCap.Core.dll.");
+ continue;
+ }
+ if (entries.Length != 1)
+ {
+ failures.Add(
+ $"The package contains {entries.Length} FlashCap.Core assemblies for {framework}; expected one.");
+ continue;
+ }
+
+ VerifyAssembly(entries[0], framework, failures);
+ }
+
+ foreach (var framework in assemblyEntries.Keys.Except(ExpectedFrameworks, StringComparer.OrdinalIgnoreCase))
+ {
+ failures.Add($"The package contains an unexpected FlashCap.Core lib asset for {framework}.");
+ }
+
+ return new PackageIdentity(version);
+ }
+
+ private static string GetFrameworkFromLibraryPath(ZipArchiveEntry entry)
+ {
+ var segments = entry.FullName.Split('/');
+ return segments.Length == 3 ? segments[1] : entry.FullName;
+ }
+
+ private static string VerifyNuspec(ZipArchive archive, List failures)
+ {
+ var nuspecEntries = archive.Entries
+ .Where(entry => entry.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+ if (nuspecEntries.Length != 1)
+ {
+ throw new InvalidDataException(
+ $"The package must contain exactly one nuspec, but contains {nuspecEntries.Length}.");
+ }
+
+ using var stream = nuspecEntries[0].Open();
+ var document = XDocument.Load(stream, LoadOptions.None);
+ var metadata = document.Descendants().FirstOrDefault(
+ element => element.Name.LocalName == "metadata") ??
+ throw new InvalidDataException("The nuspec does not contain metadata.");
+ var packageId = metadata.Elements().FirstOrDefault(
+ element => element.Name.LocalName == "id")?.Value;
+ var version = metadata.Elements().FirstOrDefault(
+ element => element.Name.LocalName == "version")?.Value;
+ if (!string.Equals(packageId, "FlashCap.Core", StringComparison.OrdinalIgnoreCase) ||
+ string.IsNullOrWhiteSpace(version))
+ {
+ throw new InvalidDataException("The package identity is not FlashCap.Core with a version.");
+ }
+ foreach (var dependency in document.Descendants().Where(
+ element => element.Name.LocalName == "dependency"))
+ {
+ var id = dependency.Attribute("id")?.Value;
+ if (id is not null && ForbiddenPackageIds.Contains(id))
+ {
+ failures.Add($"The nuspec declares the forbidden dependency {id}.");
+ }
+ }
+ return version;
+ }
+
+ private static void VerifyAssembly(
+ ZipArchiveEntry assemblyEntry,
+ string framework,
+ List failures)
+ {
+ using var assemblyImage = new MemoryStream(checked((int)assemblyEntry.Length));
+ using (var entryStream = assemblyEntry.Open())
+ {
+ entryStream.CopyTo(assemblyImage);
+ }
+ assemblyImage.Position = 0;
+
+ using var peReader = new PEReader(assemblyImage, PEStreamOptions.LeaveOpen);
+ if (!peReader.HasMetadata)
+ {
+ failures.Add($"lib/{framework}/FlashCap.Core.dll does not contain managed metadata.");
+ return;
+ }
+
+ var metadata = peReader.GetMetadataReader();
+ if (!metadata.IsAssembly)
+ {
+ failures.Add($"lib/{framework}/FlashCap.Core.dll is not an assembly.");
+ return;
+ }
+
+ VerifyPublicTypes(metadata, framework, failures);
+ VerifyAssemblyReferences(metadata, framework, failures);
+ if (!ModernMarshallingFrameworks.Contains(framework))
+ {
+ VerifyLegacyTypeReferences(metadata, framework, failures);
+ }
+ }
+
+ private static void VerifyPublicTypes(
+ MetadataReader metadata,
+ string framework,
+ List failures)
+ {
+ var publicTypes = new HashSet(StringComparer.Ordinal);
+ foreach (var handle in metadata.TypeDefinitions)
+ {
+ var definition = metadata.GetTypeDefinition(handle);
+ if ((definition.Attributes & TypeAttributes.VisibilityMask) != TypeAttributes.Public)
+ {
+ continue;
+ }
+
+ var typeNamespace = metadata.GetString(definition.Namespace);
+ var typeName = metadata.GetString(definition.Name);
+ publicTypes.Add(string.IsNullOrEmpty(typeNamespace) ? typeName : $"{typeNamespace}.{typeName}");
+ }
+
+ foreach (var requiredType in RequiredPublicTypes)
+ {
+ if (!publicTypes.Contains(requiredType))
+ {
+ failures.Add($"lib/{framework}/FlashCap.Core.dll is missing public type {requiredType}.");
+ }
+ }
+ }
+
+ private static void VerifyAssemblyReferences(
+ MetadataReader metadata,
+ string framework,
+ List failures)
+ {
+ var references = metadata.AssemblyReferences
+ .Select(handle => metadata.GetString(metadata.GetAssemblyReference(handle).Name))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ if (references.Contains("System.Runtime.CompilerServices.Unsafe"))
+ {
+ failures.Add(
+ $"lib/{framework}/FlashCap.Core.dll references System.Runtime.CompilerServices.Unsafe.");
+ }
+ if (LegacyMemoryFrameworks.Contains(framework) && references.Contains("System.Memory"))
+ {
+ failures.Add($"lib/{framework}/FlashCap.Core.dll references System.Memory.");
+ }
+ }
+
+ private static void VerifyLegacyTypeReferences(
+ MetadataReader metadata,
+ string framework,
+ List failures)
+ {
+ foreach (var handle in metadata.TypeReferences)
+ {
+ var reference = metadata.GetTypeReference(handle);
+ var typeNamespace = metadata.GetString(reference.Namespace);
+ var typeName = metadata.GetString(reference.Name);
+ if (typeNamespace.Equals(
+ "System.Runtime.InteropServices.Marshalling",
+ StringComparison.Ordinal) ||
+ typeName is "GeneratedComInterfaceAttribute" or "GeneratedComClassAttribute")
+ {
+ failures.Add(
+ $"lib/{framework}/FlashCap.Core.dll contains legacy-incompatible TypeRef " +
+ $"{typeNamespace}.{typeName}.");
+ }
+ }
+ }
+
+ private static void VerifyConsumerAssets(
+ string assetsPath,
+ PackageIdentity package,
+ List failures)
+ {
+ if (!File.Exists(assetsPath))
+ {
+ throw new FileNotFoundException("The consumer assets file does not exist.", assetsPath);
+ }
+
+ using var stream = File.OpenRead(assetsPath);
+ using var document = JsonDocument.Parse(stream);
+ if (!document.RootElement.TryGetProperty("targets", out var targets) ||
+ targets.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidDataException("The consumer assets file does not contain a targets object.");
+ }
+
+ var baseTargets = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var target in targets.EnumerateObject())
+ {
+ var baseTarget = target.Name.Split('/', 2)[0];
+ baseTargets.Add(baseTarget);
+
+ if (target.Value.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidDataException($"Consumer target {target.Name} is not an object.");
+ }
+
+ var hasFlashCapCore = false;
+ foreach (var library in target.Value.EnumerateObject())
+ {
+ var packageId = GetPackageId(library.Name);
+ if (packageId.Equals("FlashCap.Core", StringComparison.OrdinalIgnoreCase))
+ {
+ hasFlashCapCore = true;
+ var separator = library.Name.LastIndexOf('/');
+ var version = separator < 0 ? string.Empty : library.Name[(separator + 1)..];
+ if (!version.Equals(package.Version, StringComparison.OrdinalIgnoreCase))
+ {
+ failures.Add(
+ $"Consumer target {target.Name} resolved FlashCap.Core {version}; " +
+ $"expected {package.Version}.");
+ }
+ }
+ if (ForbiddenPackageIds.Contains(packageId))
+ {
+ failures.Add(
+ $"Consumer target {target.Name} contains the forbidden package {packageId}.");
+ }
+ }
+
+ if (!hasFlashCapCore)
+ {
+ failures.Add($"Consumer target {target.Name} does not contain FlashCap.Core.");
+ }
+ }
+
+ foreach (var framework in ExpectedFrameworks)
+ {
+ if (!baseTargets.Contains(framework))
+ {
+ failures.Add($"The consumer assets file is missing target {framework}.");
+ }
+ }
+ }
+
+ private static string GetPackageId(string libraryName)
+ {
+ var separator = libraryName.LastIndexOf('/');
+ return separator < 0 ? libraryName : libraryName[..separator];
+ }
+}