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]; + } +}