diff --git a/src/TeamsISO.Engine.NdiInterop/NdiNative.cs b/src/TeamsISO.Engine.NdiInterop/NdiNative.cs index f4a47fd..a3d66b2 100644 --- a/src/TeamsISO.Engine.NdiInterop/NdiNative.cs +++ b/src/TeamsISO.Engine.NdiInterop/NdiNative.cs @@ -4,13 +4,30 @@ namespace TeamsISO.Engine.NdiInterop; /// /// P/Invoke declarations for the NewTek/Vizrt NDI SDK 6 native library. -/// On Windows the import target is Processing.NDI.Lib.x64.dll; the loader resolves it -/// from the NDI Runtime installation path or from the application directory. +/// On Windows the import target is Processing.NDI.Lib.x64.dll. Resolution of +/// the DLL is handled by , which loads it from +/// the NDI Runtime installation directory exposed by the +/// NDI_RUNTIME_DIR_V<n> environment variables — the NDI installer sets +/// these but does not always add the runtime directory to PATH, so a default +/// loader-based resolution would fail with 0x8007007E on otherwise correctly +/// installed machines. /// internal static class NdiNative { private const string LibName = "Processing.NDI.Lib.x64"; + /// + /// Registers with the runtime before any + /// P/Invoke against fires. The static constructor is + /// guaranteed to run before the first access to any member of this type, which + /// includes the very first [DllImport] call site, so the resolver is + /// always in place when the loader needs it. + /// + static NdiNative() + { + NdiNativeLibraryResolver.Register(); + } + // ---- Lifecycle ---- [DllImport(LibName, EntryPoint = "NDIlib_initialize", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] diff --git a/src/TeamsISO.Engine.NdiInterop/NdiNativeLibraryResolver.cs b/src/TeamsISO.Engine.NdiInterop/NdiNativeLibraryResolver.cs new file mode 100644 index 0000000..26ada5c --- /dev/null +++ b/src/TeamsISO.Engine.NdiInterop/NdiNativeLibraryResolver.cs @@ -0,0 +1,66 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace TeamsISO.Engine.NdiInterop; + +/// +/// Resolves the NDI native library (Processing.NDI.Lib.x64.dll) from the +/// NDI Runtime install directory exposed via NDI_RUNTIME_DIR_V<n> +/// environment variables. +/// +/// The NDI installer creates these env vars but does not always add the runtime +/// directory to PATH, so a default [DllImport("Processing.NDI.Lib.x64")] +/// can fail with 0x8007007E ("the specified module could not be found") on +/// otherwise correctly installed machines. This resolver patches that gap by +/// pre-loading the DLL from the install dir before the runtime falls back to its +/// default search algorithm. +/// +/// The resolver is registered from the static constructor of , +/// which guarantees it runs before the first P/Invoke on that type fires. On +/// non-Windows the resolver short-circuits; the assembly's P/Invokes are gated by +/// [SupportedOSPlatform("windows")] and won't fire there in any case. +/// +internal static class NdiNativeLibraryResolver +{ + private const string NdiX64LibraryName = "Processing.NDI.Lib.x64"; + private const string NdiX64LibraryFile = "Processing.NDI.Lib.x64.dll"; + + /// + /// NDI runtime install-dir environment variables, in preference order. + /// V6 is what TeamsISO is built against; V5/V4 are listed as graceful fallbacks + /// for installs that pre-date V6 — the runtime probe will still report a + /// version mismatch, but at least the DLL will load and the engine can surface + /// a clear alert instead of dying with DllNotFoundException. + /// + private static readonly string[] EnvVarFallbacks = + { + "NDI_RUNTIME_DIR_V6", + "NDI_RUNTIME_DIR_V5", + "NDI_RUNTIME_DIR_V4", + }; + + internal static void Register() + { + NativeLibrary.SetDllImportResolver(typeof(NdiNativeLibraryResolver).Assembly, Resolve); + } + + private static IntPtr Resolve(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != NdiX64LibraryName) return IntPtr.Zero; + if (!OperatingSystem.IsWindows()) return IntPtr.Zero; + + foreach (var envVar in EnvVarFallbacks) + { + var dir = Environment.GetEnvironmentVariable(envVar); + if (string.IsNullOrEmpty(dir)) continue; + var path = Path.Combine(dir, NdiX64LibraryFile); + if (!File.Exists(path)) continue; + if (NativeLibrary.TryLoad(path, out var handle)) + return handle; + } + + // Fall through to default loader (PATH, app dir, etc.) — preserves the + // chance that someone added the NDI dir to PATH manually. + return IntPtr.Zero; + } +}