From 0bbd90703945f183bdb525721b95795debf1cfdd Mon Sep 17 00:00:00 2001 From: Lukas Kurz Date: Wed, 22 Nov 2023 15:51:39 +0100 Subject: [PATCH 1/2] Started work on virtual devices --- .../Program.cs | 10 + ...v.DirectShow.VirtualDevices.Console.csproj | 14 + VBAudioRouter.sln | 83 ++++++ .../DllMain.cs | 31 +++ .../FilterRegistry.cs | 53 ++++ .../GlobalUsings.cs | 2 + .../Internal/MediaTypeEnumerator.cs | 36 +++ .../Internal/PinEnumerator.cs | 42 +++ .../Mods/IAMStreamConfig.cs | 83 ++++++ .../Mods/IKsPropertySet.cs | 35 +++ .../NativeMethods.json | 8 + .../NativeMethods.txt | 50 ++++ .../OutputFilter.cs | 148 ++++++++++ .../OutputPin.cs | 255 ++++++++++++++++++ .../ShortDev.DirectShow.VirtualDevices.csproj | 18 ++ .../TestFilter.cs | 23 ++ 16 files changed, 891 insertions(+) create mode 100644 ShortDev.DirectShow.VirtualDevices.Console/Program.cs create mode 100644 ShortDev.DirectShow.VirtualDevices.Console/ShortDev.DirectShow.VirtualDevices.Console.csproj create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/FilterRegistry.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/GlobalUsings.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/Internal/MediaTypeEnumerator.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/Internal/PinEnumerator.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/Mods/IAMStreamConfig.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/Mods/IKsPropertySet.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.json create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.txt create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/OutputFilter.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/OutputPin.cs create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/ShortDev.DirectShow.VirtualDevices.csproj create mode 100644 lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs diff --git a/ShortDev.DirectShow.VirtualDevices.Console/Program.cs b/ShortDev.DirectShow.VirtualDevices.Console/Program.cs new file mode 100644 index 0000000..11c32dc --- /dev/null +++ b/ShortDev.DirectShow.VirtualDevices.Console/Program.cs @@ -0,0 +1,10 @@ +// See https://aka.ms/new-console-template for more information +using ShortDev.DirectShow.VirtualDevices; + +Console.WriteLine("Hello, World!"); + +// FilterRegistry.RegisterFilter(register: false); +FilterRegistry.RegisterFilter(register: true); +// FilterRegistry.RegisterFilter("Test LK"); + +Console.WriteLine("Ok!"); \ No newline at end of file diff --git a/ShortDev.DirectShow.VirtualDevices.Console/ShortDev.DirectShow.VirtualDevices.Console.csproj b/ShortDev.DirectShow.VirtualDevices.Console/ShortDev.DirectShow.VirtualDevices.Console.csproj new file mode 100644 index 0000000..76117b8 --- /dev/null +++ b/ShortDev.DirectShow.VirtualDevices.Console/ShortDev.DirectShow.VirtualDevices.Console.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0-windows + enable + enable + + + + + + + diff --git a/VBAudioRouter.sln b/VBAudioRouter.sln index 946d78f..9435adb 100644 --- a/VBAudioRouter.sln +++ b/VBAudioRouter.sln @@ -7,6 +7,14 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "VBAudioRouter.UWP", "VBAudi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VBAudioRouter", "VBAudioRouter\VBAudioRouter.csproj", "{479E5F66-984B-4CF5-A358-4979600292DC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShortDev.DirectShow.VirtualDevices", "lib\ShortDev.DirectShow.VirtualDevices\ShortDev.DirectShow.VirtualDevices.csproj", "{6B8394AA-D065-44C1-A927-7F5DB7B72C34}" +EndProject +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "ShortDev.DirectShow.VirtualDevices.Package", "ShortDev.DirectShow.VirtualDevices.Package\ShortDev.DirectShow.VirtualDevices.Package.wapproj", "{80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VirtualDevices", "VirtualDevices", "{ED95D1FA-7B51-4C79-99E1-0F9BA477D495}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShortDev.DirectShow.VirtualDevices.Console", "ShortDev.DirectShow.VirtualDevices.Console\ShortDev.DirectShow.VirtualDevices.Console.csproj", "{6075424B-F442-4FEF-B600-E70368EDC707}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,10 +89,85 @@ Global {479E5F66-984B-4CF5-A358-4979600292DC}.Release|x86.ActiveCfg = Release|x86 {479E5F66-984B-4CF5-A358-4979600292DC}.Release|x86.Build.0 = Release|x86 {479E5F66-984B-4CF5-A358-4979600292DC}.Release|x86.Deploy.0 = Release|x86 + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|ARM.Build.0 = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|ARM64.Build.0 = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|x64.Build.0 = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Debug|x86.Build.0 = Debug|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|Any CPU.Build.0 = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|ARM.ActiveCfg = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|ARM.Build.0 = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|ARM64.ActiveCfg = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|ARM64.Build.0 = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|x64.ActiveCfg = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|x64.Build.0 = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|x86.ActiveCfg = Release|Any CPU + {6B8394AA-D065-44C1-A927-7F5DB7B72C34}.Release|x86.Build.0 = Release|Any CPU + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|ARM.ActiveCfg = Debug|ARM + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|ARM.Build.0 = Debug|ARM + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|ARM.Deploy.0 = Debug|ARM + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|ARM64.Build.0 = Debug|ARM64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|x64.ActiveCfg = Debug|x64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|x64.Build.0 = Debug|x64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|x64.Deploy.0 = Debug|x64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|x86.ActiveCfg = Debug|x86 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|x86.Build.0 = Debug|x86 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Debug|x86.Deploy.0 = Debug|x86 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|Any CPU.Build.0 = Release|Any CPU + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|Any CPU.Deploy.0 = Release|Any CPU + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|ARM.ActiveCfg = Release|ARM + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|ARM.Build.0 = Release|ARM + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|ARM.Deploy.0 = Release|ARM + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|ARM64.ActiveCfg = Release|ARM64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|ARM64.Build.0 = Release|ARM64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|ARM64.Deploy.0 = Release|ARM64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|x64.ActiveCfg = Release|x64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|x64.Build.0 = Release|x64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|x64.Deploy.0 = Release|x64 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|x86.ActiveCfg = Release|x86 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|x86.Build.0 = Release|x86 + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6}.Release|x86.Deploy.0 = Release|x86 + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|ARM.Build.0 = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|ARM64.Build.0 = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|x64.ActiveCfg = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|x64.Build.0 = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|x86.ActiveCfg = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Debug|x86.Build.0 = Debug|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|Any CPU.Build.0 = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|ARM.ActiveCfg = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|ARM.Build.0 = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|ARM64.ActiveCfg = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|ARM64.Build.0 = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|x64.ActiveCfg = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|x64.Build.0 = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|x86.ActiveCfg = Release|Any CPU + {6075424B-F442-4FEF-B600-E70368EDC707}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6B8394AA-D065-44C1-A927-7F5DB7B72C34} = {ED95D1FA-7B51-4C79-99E1-0F9BA477D495} + {80B1E1CA-DDA0-4DD3-9901-A86DE68E81B6} = {ED95D1FA-7B51-4C79-99E1-0F9BA477D495} + {6075424B-F442-4FEF-B600-E70368EDC707} = {ED95D1FA-7B51-4C79-99E1-0F9BA477D495} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {94079978-D490-48BD-8069-F095B27A6654} EndGlobalSection diff --git a/lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs b/lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs new file mode 100644 index 0000000..86eb881 --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs @@ -0,0 +1,31 @@ +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; + +namespace ShortDev.DirectShow.VirtualDevices; + +internal class DllMain +{ + [UnmanagedCallersOnly(EntryPoint = nameof(DllCanUnloadNow))] + public static HRESULT DllCanUnloadNow() + { + return HRESULT.S_FALSE; + } + + [UnmanagedCallersOnly(EntryPoint = nameof(DllGetClassObject))] + public static unsafe HRESULT DllGetClassObject(Guid* rclsid, Guid* riid, void** ppv) + { + var clsid = *rclsid; + var iid = *riid; + + if (clsid != typeof(TestFilter).GUID) + return HRESULT.S_FALSE; + + var pUnk = Marshal.GetIUnknownForObject(new TestFilter()); + var hr = (HRESULT)Marshal.QueryInterface(pUnk, ref iid, out var result); + hr.ThrowOnFailure(); + + *ppv = (void*)result; + + return HRESULT.S_OK; + } +} diff --git a/lib/ShortDev.DirectShow.VirtualDevices/FilterRegistry.cs b/lib/ShortDev.DirectShow.VirtualDevices/FilterRegistry.cs new file mode 100644 index 0000000..96b9cce --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/FilterRegistry.cs @@ -0,0 +1,53 @@ +using Windows.Win32.System.Com; + +namespace ShortDev.DirectShow.VirtualDevices; + +public static class FilterRegistry +{ + public static unsafe void RegisterFilter(bool register) + { + CoCreateInstance(CLSID_FilterMapper2, null, CLSCTX.CLSCTX_INPROC_SERVER, out var mapper).ThrowOnFailure(); + + Guid mediType = MEDIATYPE_Audio; + REGPINTYPES types = new() + { + clsMajorType = &mediType, + // clsMinorType = MEDIASUBTYPE_NULL + }; + REGFILTERPINS pinInfo = new() + { + bMany = false, + bOutput = false, + bRendered = true, + bZero = false, + nMediaTypes = 1, + lpMediaType = &types + }; + REGFILTER2 filter = new() + { + dwVersion = 1, + dwMerit = (uint)IFILTERMAPPER_MERIT.MERIT_NORMAL, + Anonymous = new() + { + Anonymous1 = new() + { + cPins = 1, + rgPins = &pinInfo + } + } + }; + + Type type = typeof(T); + var clsid = type.GUID; + var category = CLSID_AudioInputDeviceCategory; + fixed (char* pName = type.Name) + fixed (char* pInstanceName = clsid.ToString("B")) + { + if (register) + mapper.RegisterFilter(&clsid, pName, null, &category, pInstanceName, &filter).ThrowOnFailure(); + else + mapper.UnregisterFilter(&category, pInstanceName, &clsid).ThrowOnFailure(); + } + + } +} diff --git a/lib/ShortDev.DirectShow.VirtualDevices/GlobalUsings.cs b/lib/ShortDev.DirectShow.VirtualDevices/GlobalUsings.cs new file mode 100644 index 0000000..6693edb --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Windows.Win32.Media.DirectShow; +global using static Windows.Win32.PInvoke; \ No newline at end of file diff --git a/lib/ShortDev.DirectShow.VirtualDevices/Internal/MediaTypeEnumerator.cs b/lib/ShortDev.DirectShow.VirtualDevices/Internal/MediaTypeEnumerator.cs new file mode 100644 index 0000000..fb21e3c --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/Internal/MediaTypeEnumerator.cs @@ -0,0 +1,36 @@ +using Windows.Win32.Foundation; +using Windows.Win32.Media.MediaFoundation; + +namespace ShortDev.DirectShow.VirtualDevices.Internal; + +internal sealed class MediaTypeEnumerator : IEnumMediaTypes +{ + readonly OutputPin _pin; + public MediaTypeEnumerator(OutputPin pin) + => _pin = pin; + + uint _index = 0; + HRESULT IEnumMediaTypes.Next(uint cMediaTypes, AM_MEDIA_TYPE[][] ppMediaTypes, out uint pcFetched) + { + ppMediaTypes[0] = new[] { _pin.MediaType }; + + pcFetched = ++_index; + + return pcFetched > 1 ? HRESULT.S_FALSE : HRESULT.S_OK; + } + + HRESULT IEnumMediaTypes.Skip(uint cMediaTypes) + => HRESULT.S_FALSE; + + HRESULT IEnumMediaTypes.Reset() + { + _index = 0; + return HRESULT.S_OK; + } + + HRESULT IEnumMediaTypes.Clone(out IEnumMediaTypes ppEnum) + { + ppEnum = new MediaTypeEnumerator(_pin); + return HRESULT.S_OK; + } +} diff --git a/lib/ShortDev.DirectShow.VirtualDevices/Internal/PinEnumerator.cs b/lib/ShortDev.DirectShow.VirtualDevices/Internal/PinEnumerator.cs new file mode 100644 index 0000000..dad2d4a --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/Internal/PinEnumerator.cs @@ -0,0 +1,42 @@ +using Windows.Win32.Foundation; + +namespace ShortDev.DirectShow.VirtualDevices.Internal; + +internal sealed class PinEnumerator : IEnumPins +{ + readonly OutputFilter _filter; + public PinEnumerator(OutputFilter filter) + => _filter = filter; + + uint _index = 0; + HRESULT IEnumPins.Next(uint cPins, IPin[] ppPins, out uint pcFetched) + { + ppPins[0] = _filter.Pin; + ++_index; + test(out pcFetched); + return _index > 1 ? HRESULT.S_FALSE : HRESULT.S_OK; + + static unsafe void test(out uint test) + { + fixed (uint* pTest = &test) + { + + } + } + } + + HRESULT IEnumPins.Skip(uint cPins) + => HRESULT.S_FALSE; + + HRESULT IEnumPins.Reset() + { + _index = 0; + return HRESULT.S_OK; + } + + HRESULT IEnumPins.Clone(out IEnumPins ppEnum) + { + ppEnum = new PinEnumerator(_filter); + return HRESULT.S_OK; + } +} diff --git a/lib/ShortDev.DirectShow.VirtualDevices/Mods/IAMStreamConfig.cs b/lib/ShortDev.DirectShow.VirtualDevices/Mods/IAMStreamConfig.cs new file mode 100644 index 0000000..d021518 --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/Mods/IAMStreamConfig.cs @@ -0,0 +1,83 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ + +#pragma warning disable CS1591,CS1573,CS0465,CS0649,CS8019,CS1570,CS1584,CS1658,CS0436,CS8981 +using global::System; +using global::System.Diagnostics; +using global::System.Diagnostics.CodeAnalysis; +using global::System.Runtime.CompilerServices; +using global::System.Runtime.InteropServices; +using global::System.Runtime.Versioning; +using winmdroot = global::Windows.Win32; +namespace Windows.Win32 +{ + namespace Media.DirectShow + { + [Guid("C6E13340-30AC-11D0-A18C-00A0C9118956"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ComImport()] + [SupportedOSPlatform("windows5.0")] + internal interface IAMStreamConfig_Mod + { + /// The SetFormat method sets the output format on the pin. + /// Pointer to an AM_MEDIA_TYPE structure that specifies the new format. + /// + /// Returns an HRESULT value. Possible values include the following. + /// This doc was truncated. + /// + /// + /// This method specifies the format for the output pin. If the pin is not connected, it will use this format for its next connection. If the pin is already connected, it will attempt to reconnect with this format. The method might fail if the other pin rejects the new type. If this method succeeds, subsequent calls to the IPin::EnumMediaTypes method will return the new type, and no others. On most filters, this method fails if the filter is paused or running. On some compression filters, the method fails if the filter's input pin is not connected. With some filters, you can call this method with the value NULL to reset the pin to its default format. Filter Developers: The following remarks describe how to implement this method: If the output pin is not connected, and the pin supports the specified media type, return S_OK. Store the media type and offer it as format number zero in the CBasePin::GetMediaType method. Do not offer other formats, and reject other formats in the CBasePin::CheckMediaType method. If the pin is already connected, and the pin supports the media type, reconnect the pin with that type. If the other pin rejects the new type, return VFW_E_INVALIDMEDIATYPE and restore the original connection. + /// Read more on docs.microsoft.com. + /// + [PreserveSig()] + winmdroot.Foundation.HRESULT SetFormat(in winmdroot.Media.MediaFoundation.AM_MEDIA_TYPE pmt); + + /// The GetFormat method retrieves the current or preferred output format. + /// Address of a pointer to an AM_MEDIA_TYPE structure. + /// + /// Returns an HRESULT value. Possible values include the following. + /// This doc was truncated. + /// + /// + /// If the pin is connected, this method returns the format that the pin is currently using. Otherwise, the method returns the pin's preferred format for the next pin connection. If you have already called the IAMStreamConfig::SetFormat method to set the format, GetFormat returns the same format. If not, it returns the first format in the pin's list of preferred formats, as determined by the IPin::EnumMediaTypes method. The method allocates the memory for the AM_MEDIA_TYPE structure, fills in the structure, and returns it in the pmt parameter. The caller must release the memory, including the format block. You can use the DeleteMediaType helper function in the base class library. On some compression filters, the method fails if the filter's input pin is not connected. + /// Read more on docs.microsoft.com. + /// + [PreserveSig()] + winmdroot.Foundation.HRESULT GetFormat(out winmdroot.Media.MediaFoundation.AM_MEDIA_TYPE[] ppmt); + + /// The GetNumberOfCapabilities method retrieves the number of format capabilities that this pin supports. + /// Pointer to a variable that receives the number of format capabilities. + /// Pointer to a variable that receives the size of the configuration structure in bytes. See Remarks for more information. + /// + /// Returns an HRESULT value. Possible values include the following. + /// This doc was truncated. + /// + /// + /// An output pin can support more than one set of format capabilities. This method returns the total number of capabilities that the pin supports; the number is returned in the piCount parameter. To retrieve a particular set of capabilities, call the IAMStreamConfig::GetStreamCaps method. Format capabilities are indexed from zero, so the value returned in piCount is one more than the upper bound. Depending on the pin's format type, the [VIDEO_STREAM_CONFIG_CAPS](/windows/desktop/api/strmif/ns-strmif-video_stream_config_caps) structure (for video) or an [AUDIO_STREAM_CONFIG_CAPS](/windows/desktop/api/strmif/ns-strmif-audio_stream_config_caps) structure (for audio). The piSize parameter receives the size of the structure, in bytes. On some compression filters, this method fails if the filter's input pin is not connected. + /// Read more on docs.microsoft.com. + /// + [PreserveSig()] + winmdroot.Foundation.HRESULT GetNumberOfCapabilities(out int piCount, out int piSize); + + /// The GetStreamCaps method retrieves a set of format capabilities. + /// Specifies the format capability to retrieve, indexed from zero. To determine the number of capabilities that the pin supports, call the IAMStreamConfig::GetNumberOfCapabilities method. + /// Address of a pointer to an AM_MEDIA_TYPE structure. The method allocates the structure and fills it with a media type. + /// Pointer to a byte array allocated by the caller. For video, use the VIDEO_STREAM_CONFIG_CAPS structure (see Remarks). For audio, use the AUDIO_STREAM_CONFIG_CAPS structure. To determine the required size of the array, call the GetNumberOfCapabilities method. The size is returned in the piSize parameter. + /// + /// Returns an HRESULT value. Possible values include the following. + /// This doc was truncated. + /// + /// + /// This method returns two pieces of information: + /// This doc was truncated. + /// Read more on docs.microsoft.com. + /// + [PreserveSig()] + unsafe winmdroot.Foundation.HRESULT GetStreamCaps(int iIndex, out winmdroot.Media.MediaFoundation.AM_MEDIA_TYPE[] ppmt, void* pSCC); + } + } +} diff --git a/lib/ShortDev.DirectShow.VirtualDevices/Mods/IKsPropertySet.cs b/lib/ShortDev.DirectShow.VirtualDevices/Mods/IKsPropertySet.cs new file mode 100644 index 0000000..b73161c --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/Mods/IKsPropertySet.cs @@ -0,0 +1,35 @@ + +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Windows.Win32.Media.KernelStreaming; + +[Guid("C6E13340-30AC-11D0-A18C-00A0C9118956"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ComImport()] +[SupportedOSPlatform("windows5.0")] +internal unsafe interface IKsPropertySet +{ + Foundation.HRESULT Set( + in Guid PropSet, + ulong Id, + void* InstanceData, + ulong InstanceLength, + void* PropertyData, + ulong DataLength + ); + + Foundation.HRESULT Get( + in Guid guidPropSet, + in uint dwPropID, + void* pInstanceData, + in uint cbInstanceData, + void* pPropData, + in uint cbPropData, + out uint pcbReturned + ); + + Foundation.HRESULT QuerySupported( + in Guid PropSet, + ulong Id, + ulong* TypeSupport + ); +} \ No newline at end of file diff --git a/lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.json b/lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.json new file mode 100644 index 0000000..9e5f8f8 --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "comInterop": { + "preserveSigMethods": [ "*" ] + }, + "allowMarshaling": true, + "public": true +} \ No newline at end of file diff --git a/lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.txt b/lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.txt new file mode 100644 index 0000000..2df0769 --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/NativeMethods.txt @@ -0,0 +1,50 @@ +IBaseFilter +IPin +IMemInputPin +IAMStreamConfig +IEnumPins +IEnumMediaTypes + +IKsPropertySet +AMPROPSETID_Pin +AMPROPERTY_PIN +PIN_CATEGORY_CAPTURE +KSPROPERTY_SUPPORT_GET + +CoCreateInstance +CLSID_MemoryAllocator +CLSCTX_INPROC_SERVER +IMemAllocator +AM_GBF_NOWAIT +IMediaSample + +WAVEFORMATEX +FORMAT_WaveFormatEx +MEDIASUBTYPE_PCM +WAVE_FORMAT_PCM + +AUDIO_STREAM_CONFIG_CAPS +AUDIOINFOHEADER +MEDIATYPE_Audio +MEDIASUBTYPE_NULL + +IFilterMapper2 +CLSID_FilterMapper2 +MERIT_NORMAL +CLSID_LegacyAmFilterCategory +CLSID_AudioInputDeviceCategory +CLSID_AudioRendererCategory + +S_OK +S_FALSE +E_FAIL +E_NOTIMPL +E_INVALIDARG + +VFW_E_NOT_FOUND +VFW_E_NOT_STOPPED +VFW_E_ALREADY_CONNECTED +VFW_E_NOT_CONNECTED +VFW_E_NO_ALLOCATOR + +E_PROP_SET_UNSUPPORTED \ No newline at end of file diff --git a/lib/ShortDev.DirectShow.VirtualDevices/OutputFilter.cs b/lib/ShortDev.DirectShow.VirtualDevices/OutputFilter.cs new file mode 100644 index 0000000..bef577f --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/OutputFilter.cs @@ -0,0 +1,148 @@ +using ShortDev.DirectShow.VirtualDevices.Internal; +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.Media; +using Windows.Win32.Media.Audio; +using Windows.Win32.Media.MediaFoundation; + +namespace ShortDev.DirectShow.VirtualDevices; + +// https://github.com/obsproject/libdshowcapture/blob/master/source/output-filter.cpp + +public abstract class OutputFilter : IBaseFilter +{ + public abstract string Name { get; } + public abstract bool IsOutput { get; } + + public OutputPin Pin { get; } + public AM_MEDIA_TYPE MediaType { get; } + public OutputFilter(AM_MEDIA_TYPE mediaType) + { + Pin = new(this, mediaType); + MediaType = mediaType; + } + + IFilterGraph? _graph; + + public FILTER_STATE State { get; private set; } = FILTER_STATE.State_Stopped; + + public unsafe HRESULT GetState(uint dwMilliSecsTimeout, FILTER_STATE* state) + { + *state = State; + + return HRESULT.S_OK; + } + + public HRESULT Stop() + { + if (State != FILTER_STATE.State_Stopped) + Pin.Flush(); + + State = FILTER_STATE.State_Stopped; + + return HRESULT.S_OK; + } + + public HRESULT Pause() + { + // ToDo: Implementation?! + + State = FILTER_STATE.State_Paused; + + return HRESULT.S_OK; + } + + public HRESULT Run(long tStart) + { + State = FILTER_STATE.State_Running; + + return HRESULT.S_OK; + } + + #region IReferenceClock + IReferenceClock? _clock; + public HRESULT SetSyncSource(IReferenceClock pClock) + { + _clock = pClock; + + return HRESULT.S_OK; + } + + public HRESULT GetSyncSource(out IReferenceClock? pClock) + { + pClock = _clock; + + return HRESULT.S_OK; + } + #endregion + + HRESULT IBaseFilter.EnumPins(out IEnumPins ppEnum) + { + ppEnum = new PinEnumerator(this); + + return HRESULT.S_OK; + } + + HRESULT IBaseFilter.FindPin(PCWSTR Id, out IPin? ppPin) + { + ppPin = null; + + var id = Id.ToString(); + if (id != Pin.Name) + return HRESULT.VFW_E_NOT_FOUND; + + ppPin = Pin; + + return HRESULT.S_OK; + } + + HRESULT IBaseFilter.QueryFilterInfo(out FILTER_INFO pInfo) + { + pInfo.achName = Name; + pInfo.pGraph = _graph; // ToDo: AddRef ?! + + return HRESULT.S_OK; + } + + HRESULT IBaseFilter.JoinFilterGraph(IFilterGraph pGraph, PCWSTR pName) + { + _graph = pGraph; + + return HRESULT.S_OK; + } + + #region Not Implemented + public unsafe HRESULT GetClassID(Guid* pClassID) + => HRESULT.E_NOTIMPL; + + unsafe HRESULT IBaseFilter.QueryVendorInfo(PWSTR* pVendorInfo) + => HRESULT.E_NOTIMPL; + #endregion + + public static unsafe AM_MEDIA_TYPE CreateMediaType() + { + AM_MEDIA_TYPE result = default; + result.majortype = MEDIATYPE_Audio; + result.subtype = MEDIASUBTYPE_PCM; + + var cbFormat = sizeof(WAVEFORMATEX); + var pFormat = (WAVEFORMATEX*)Marshal.AllocCoTaskMem(cbFormat); + + pFormat->wFormatTag = (ushort)WAVE_FORMAT_PCM; + pFormat->nChannels = 2; + pFormat->wBitsPerSample = 32; + pFormat->nSamplesPerSec = 48000; + pFormat->nBlockAlign = (ushort)(pFormat->nChannels * pFormat->wBitsPerSample / 8); + pFormat->nAvgBytesPerSec = pFormat->nBlockAlign * pFormat->nSamplesPerSec; + pFormat->cbSize = 0; + + result.formattype = FORMAT_WaveFormatEx; + result.cbFormat = (uint)cbFormat; + result.pbFormat = (byte*)pFormat; + + result.bFixedSizeSamples = true; + // result.lSampleSize = ; + + return result; + } +} diff --git a/lib/ShortDev.DirectShow.VirtualDevices/OutputPin.cs b/lib/ShortDev.DirectShow.VirtualDevices/OutputPin.cs new file mode 100644 index 0000000..e5a453a --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/OutputPin.cs @@ -0,0 +1,255 @@ +using ShortDev.DirectShow.VirtualDevices.Internal; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.Media.KernelStreaming; +using Windows.Win32.Media.MediaFoundation; +using Windows.Win32.System.Com; + +namespace ShortDev.DirectShow.VirtualDevices; + +public sealed class OutputPin : IPin, IAMStreamConfig_Mod, IKsPropertySet +{ + public AM_MEDIA_TYPE MediaType { get; private set; } + + readonly OutputFilter _filter; + public OutputPin(OutputFilter filter, AM_MEDIA_TYPE mediaType) + { + _filter = filter; + MediaType = mediaType; + } + + public string Name { get; } = nameof(OutputPin); + + public PIN_DIRECTION Direction + => _filter.IsOutput ? PIN_DIRECTION.PINDIR_INPUT : PIN_DIRECTION.PINDIR_OUTPUT; + + IPin? _connectedPin; + HRESULT IPin.Connect(IPin pReceivePin, in AM_MEDIA_TYPE pmt) + { + if (_filter.State == FILTER_STATE.State_Running) + return HRESULT.VFW_E_NOT_STOPPED; + + if (_connectedPin != null) + return HRESULT.VFW_E_ALREADY_CONNECTED; + + if (!pReceivePin.ReceiveConnection(this, MediaType).Succeeded) + return HRESULT.E_FAIL; + + if (!TryAllocateBuffers(pReceivePin)) + return HRESULT.E_FAIL; + + _connectedPin = pReceivePin; + return HRESULT.S_OK; + } + + HRESULT IPin.ReceiveConnection(IPin pConnector, in AM_MEDIA_TYPE pmt) + => HRESULT.S_OK; + + public HRESULT Disconnect() + { + if (_connectedPin == null) + return HRESULT.S_FALSE; + + _allocator?.Decommit(); + _allocator = null; + + _connectedPin = null; + return HRESULT.S_OK; + } + + public HRESULT ConnectedTo(out IPin? pPin) + { + pPin = null; + + if (_connectedPin == null) + return HRESULT.VFW_E_NOT_CONNECTED; + + pPin = _connectedPin; + + return HRESULT.S_OK; + } + + public HRESULT ConnectionMediaType(out AM_MEDIA_TYPE pmt) + { + pmt = default; + + if (_connectedPin == null) + return HRESULT.VFW_E_NOT_CONNECTED; + + pmt = MediaType; // ToDo: Copy?! + return HRESULT.S_OK; + } + + public HRESULT QueryPinInfo(out PIN_INFO pInfo) + { + pInfo.pFilter = _filter; + pInfo.achName = Name; + pInfo.dir = Direction; + + return HRESULT.S_OK; + } + + public unsafe HRESULT QueryDirection(PIN_DIRECTION* pPinDir) + { + *pPinDir = Direction; + return HRESULT.S_OK; + } + + public unsafe HRESULT QueryId(PWSTR* Id) + { + fixed (char* pIdStr = Name) + *Id = new PWSTR(pIdStr); + + return HRESULT.S_OK; + } + + public HRESULT QueryAccept(in AM_MEDIA_TYPE pmt) + => HRESULT.S_OK; + + public HRESULT EnumMediaTypes(out IEnumMediaTypes ppEnum) + { + ppEnum = new MediaTypeEnumerator(this); + + return HRESULT.S_OK; + } + + public HRESULT QueryInternalConnections([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1), Out] IPin[] apPin, ref uint nPin) + => HRESULT.E_NOTIMPL; + + public HRESULT EndOfStream() + => HRESULT.S_OK; + + bool _flusing = false; + public HRESULT BeginFlush() + { + _flusing = true; + + return HRESULT.S_OK; + } + + public HRESULT EndFlush() + { + _flusing = false; + + return HRESULT.S_OK; + } + + public HRESULT NewSegment(long tStart, long tStop, double dRate) + => HRESULT.S_OK; + + public HRESULT SetFormat(in AM_MEDIA_TYPE pmt) + { + MediaType = pmt; + + return HRESULT.S_OK; + } + + public HRESULT GetFormat(out AM_MEDIA_TYPE[] ppmt) + { + ppmt = new[] { MediaType }; // ToDo: Copy?! + + return HRESULT.S_OK; + } + + public unsafe HRESULT GetNumberOfCapabilities(out int piCount, out int piSize) + { + piCount = 1; + piSize = sizeof(AUDIO_STREAM_CONFIG_CAPS); + + return HRESULT.S_OK; + } + + public unsafe HRESULT GetStreamCaps(int iIndex, out AM_MEDIA_TYPE[] ppmt, void* pSCC) + { + ppmt = new[] { MediaType }; + + if (iIndex != 0) + return HRESULT.E_INVALIDARG; + + AUDIO_STREAM_CONFIG_CAPS caps = *(AUDIO_STREAM_CONFIG_CAPS*)pSCC; + caps.guid = MEDIATYPE_Audio; + // ToDo: Add stuff + + return HRESULT.S_OK; + } + + public void Flush() + { + if (_connectedPin == null) + return; + + _connectedPin.BeginFlush(); + _connectedPin.EndFlush(); + } + + IMemAllocator? _allocator; + bool TryAllocateBuffers(IPin target) + { + _allocator?.Decommit(); + + var memPin = (IMemInputPin)target; + + var hr = memPin.GetAllocator(out _allocator); + if (hr != HRESULT.S_OK) + hr = CoCreateInstance(CLSID_MemoryAllocator, null, CLSCTX.CLSCTX_INPROC_SERVER, out _allocator); + + if (hr.Failed || _allocator == null) + return false; + + // ToDo: Set properties + //if (_allocator.SetProperties().Failed) + // return false; + + _allocator.Commit(); + + memPin.NotifyAllocator(_allocator, false); + + return true; + } + + public bool TryGetBuffer([MaybeNullWhen(false)] out IMediaSample sample, long startTime = 0, long endTime = 0, uint flags = AM_GBF_NOWAIT) + { + sample = null; + + if (_allocator == null) + return false; + + return _allocator.GetBuffer(out sample, startTime, endTime, flags).Succeeded; + } + + #region PropertySet + unsafe HRESULT IKsPropertySet.Set(in Guid PropSet, ulong Id, void* InstanceData, ulong InstanceLength, void* PropertyData, ulong DataLength) + => HRESULT.E_NOTIMPL; + + unsafe HRESULT IKsPropertySet.Get(in Guid guidPropSet, in uint dwPropID, void* pInstanceData, in uint cbInstanceData, void* pPropData, in uint cbPropData, out uint pcbReturned) + { + pcbReturned = 0; + + if (guidPropSet != AMPROPSETID_Pin) + return HRESULT.E_PROP_SET_UNSUPPORTED; + + if ((AMPROPERTY_PIN)dwPropID != AMPROPERTY_PIN.AMPROPERTY_PIN_CATEGORY) + return HRESULT.E_PROP_SET_UNSUPPORTED; + + pcbReturned = (uint)sizeof(Guid); + *(Guid*)pPropData = PIN_CATEGORY_CAPTURE; + + return HRESULT.S_OK; + } + + unsafe HRESULT IKsPropertySet.QuerySupported(in Guid PropSet, ulong Id, ulong* TypeSupport) + { + if (PropSet != AMPROPSETID_Pin) + return HRESULT.E_PROP_SET_UNSUPPORTED; + + if ((AMPROPERTY_PIN)Id != AMPROPERTY_PIN.AMPROPERTY_PIN_CATEGORY) + return HRESULT.E_PROP_SET_UNSUPPORTED; + + if (TypeSupport != (void*)0) + *TypeSupport = KSPROPERTY_SUPPORT_GET; + + return HRESULT.S_OK; + } + #endregion +} diff --git a/lib/ShortDev.DirectShow.VirtualDevices/ShortDev.DirectShow.VirtualDevices.csproj b/lib/ShortDev.DirectShow.VirtualDevices/ShortDev.DirectShow.VirtualDevices.csproj new file mode 100644 index 0000000..85b629d --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/ShortDev.DirectShow.VirtualDevices.csproj @@ -0,0 +1,18 @@ + + + + Library + net7.0-windows + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs b/lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs new file mode 100644 index 0000000..506dabb --- /dev/null +++ b/lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace ShortDev.DirectShow.VirtualDevices; + +[ComVisible(true)] +[Guid("C2738EF7-CA39-4CC7-B80E-74A8E2EFD411")] +public sealed class TestFilter : OutputFilter +{ + public TestFilter() : base(CreateMediaType()) + { + } + + public override string Name + => nameof(TestFilter); + + public override bool IsOutput { get; } = false; + + public void Run() + { + + } +} From 42f541b077c7be6190ddbb5ae209245b2e6328a5 Mon Sep 17 00:00:00 2001 From: Lukas Kurz Date: Sun, 23 Jun 2024 19:59:59 +0200 Subject: [PATCH 2/2] Tests --- ShortDev.DirectShow.VirtualDevices.Console/Program.cs | 3 +-- lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs | 2 +- lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ShortDev.DirectShow.VirtualDevices.Console/Program.cs b/ShortDev.DirectShow.VirtualDevices.Console/Program.cs index 11c32dc..5175f15 100644 --- a/ShortDev.DirectShow.VirtualDevices.Console/Program.cs +++ b/ShortDev.DirectShow.VirtualDevices.Console/Program.cs @@ -1,5 +1,4 @@ -// See https://aka.ms/new-console-template for more information -using ShortDev.DirectShow.VirtualDevices; +using ShortDev.DirectShow.VirtualDevices; Console.WriteLine("Hello, World!"); diff --git a/lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs b/lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs index 86eb881..286225c 100644 --- a/lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs +++ b/lib/ShortDev.DirectShow.VirtualDevices/DllMain.cs @@ -3,7 +3,7 @@ namespace ShortDev.DirectShow.VirtualDevices; -internal class DllMain +internal static class DllMain { [UnmanagedCallersOnly(EntryPoint = nameof(DllCanUnloadNow))] public static HRESULT DllCanUnloadNow() diff --git a/lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs b/lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs index 506dabb..b764dbc 100644 --- a/lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs +++ b/lib/ShortDev.DirectShow.VirtualDevices/TestFilter.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace ShortDev.DirectShow.VirtualDevices;