Compare commits

...

3 commits

Author SHA1 Message Date
d90ebb826f fix(ndi): match the NDI 6 WIN64 runtime banner in version probe
Some checks failed
CI / build-and-test (push) Failing after 26s
Shipping NDI 6 reports its version as a build banner of the form

    NDI SDK WIN64 13:07:00 Jun  2 2025 6.2.0.3

not the 'NDI SDK for Windows v6.x.x.x' format the prefix constant assumed. As a result NdiRuntimeProbe raised a spurious mismatch alert on every supported install. Update ExpectedRuntimeVersionPrefix to 'NDI SDK WIN64' which is the stable architecture token in the new banner; the trailing four-part version remains available for a stricter major-version check if that becomes useful.
2026-05-07 15:15:03 -04:00
d14a33a0a3 fix(ndi): resolve Processing.NDI.Lib.x64 via NDI_RUNTIME_DIR_V6 env var
The NDI 6 installer sets NDI_RUNTIME_DIR_V6 (and V5/V4 for back-compat) but does not always add the runtime directory to PATH, so a default DllImport(Processing.NDI.Lib.x64) failed with 0x8007007E (DllNotFoundException) on otherwise correctly installed machines, killing both TeamsISO.exe and TeamsISO.Console at preflight.

Add NdiNativeLibraryResolver, registered from NdiNative's static ctor, that resolves the DLL by trying NDI_RUNTIME_DIR_V6 / V5 / V4 in order, NativeLibrary.Load-ing the file from disk before the runtime falls back to its default search algorithm. Static-ctor registration (rather than [ModuleInitializer]) sidesteps CA2255 under TreatWarningsAsErrors and still guarantees the resolver is in place before the first P/Invoke fires.
2026-05-07 15:14:54 -04:00
0f03c272ad fix(build): use forward-slash paths in TeamsISO.sln so Windows .slnf resolves
MSBuild on Windows compares solution-filter project paths against .sln entries as raw strings, so the forward-slash entries in TeamsISO.Windows.slnf / TeamsISO.Linux.slnf were being rejected (MSB5028) against the .sln's backslash entries. Linux dotnet normalizes separators so CI happened to be green. Switch the .sln to forward slashes so both platforms agree; Visual Studio accepts either form.
2026-05-07 15:14:42 -04:00
4 changed files with 100 additions and 12 deletions

View file

@ -5,19 +5,19 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{46E05E34-8A87-4986-87D3-FE0DE4E05F44}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{46E05E34-8A87-4986-87D3-FE0DE4E05F44}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine", "src\TeamsISO.Engine\TeamsISO.Engine.csproj", "{F0D24EAE-9225-4DC4-B3D2-6966077287A0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine", "src/TeamsISO.Engine/TeamsISO.Engine.csproj", "{F0D24EAE-9225-4DC4-B3D2-6966077287A0}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.NdiInterop", "src\TeamsISO.Engine.NdiInterop\TeamsISO.Engine.NdiInterop.csproj", "{E737E54B-73DE-4F74-909C-1F0F5CF82AC6}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.NdiInterop", "src/TeamsISO.Engine.NdiInterop/TeamsISO.Engine.NdiInterop.csproj", "{E737E54B-73DE-4F74-909C-1F0F5CF82AC6}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DBDF4A1D-4215-42D5-B456-2CE7159DF848}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DBDF4A1D-4215-42D5-B456-2CE7159DF848}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.Tests", "src\tests\TeamsISO.Engine.Tests\TeamsISO.Engine.Tests.csproj", "{F8DBD7AB-E160-4B75-88FC-BAECDD4D44E8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.Tests", "src/tests/TeamsISO.Engine.Tests/TeamsISO.Engine.Tests.csproj", "{F8DBD7AB-E160-4B75-88FC-BAECDD4D44E8}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.App", "src\TeamsISO.App\TeamsISO.App.csproj", "{80DCE039-3BBC-4D3F-B44B-51F324591C29}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.App", "src/TeamsISO.App/TeamsISO.App.csproj", "{80DCE039-3BBC-4D3F-B44B-51F324591C29}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.IntegrationTests", "src\tests\TeamsISO.Engine.IntegrationTests\TeamsISO.Engine.IntegrationTests.csproj", "{A85E331D-026E-4BDE-B89C-0CC4C95001CE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Engine.IntegrationTests", "src/tests/TeamsISO.Engine.IntegrationTests/TeamsISO.Engine.IntegrationTests.csproj", "{A85E331D-026E-4BDE-B89C-0CC4C95001CE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Console", "src\TeamsISO.Console\TeamsISO.Console.csproj", "{C3254998-9428-4264-A8FB-EAC9E1F9F432}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamsISO.Console", "src/TeamsISO.Console/TeamsISO.Console.csproj", "{C3254998-9428-4264-A8FB-EAC9E1F9F432}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -4,13 +4,30 @@ namespace TeamsISO.Engine.NdiInterop;
/// <summary> /// <summary>
/// P/Invoke declarations for the NewTek/Vizrt NDI SDK 6 native library. /// P/Invoke declarations for the NewTek/Vizrt NDI SDK 6 native library.
/// On Windows the import target is <c>Processing.NDI.Lib.x64.dll</c>; the loader resolves it /// On Windows the import target is <c>Processing.NDI.Lib.x64.dll</c>. Resolution of
/// from the NDI Runtime installation path or from the application directory. /// the DLL is handled by <see cref="NdiNativeLibraryResolver"/>, which loads it from
/// the NDI Runtime installation directory exposed by the
/// <c>NDI_RUNTIME_DIR_V&lt;n&gt;</c> environment variables — the NDI installer sets
/// these but does not always add the runtime directory to <c>PATH</c>, so a default
/// loader-based resolution would fail with <c>0x8007007E</c> on otherwise correctly
/// installed machines.
/// </summary> /// </summary>
internal static class NdiNative internal static class NdiNative
{ {
private const string LibName = "Processing.NDI.Lib.x64"; private const string LibName = "Processing.NDI.Lib.x64";
/// <summary>
/// Registers <see cref="NdiNativeLibraryResolver"/> with the runtime before any
/// P/Invoke against <see cref="LibName"/> fires. The static constructor is
/// guaranteed to run before the first access to any member of this type, which
/// includes the very first <c>[DllImport]</c> call site, so the resolver is
/// always in place when the loader needs it.
/// </summary>
static NdiNative()
{
NdiNativeLibraryResolver.Register();
}
// ---- Lifecycle ---- // ---- Lifecycle ----
[DllImport(LibName, EntryPoint = "NDIlib_initialize", CallingConvention = CallingConvention.Cdecl)] [DllImport(LibName, EntryPoint = "NDIlib_initialize", CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.U1)] [return: MarshalAs(UnmanagedType.U1)]

View file

@ -0,0 +1,66 @@
using System.Reflection;
using System.Runtime.InteropServices;
namespace TeamsISO.Engine.NdiInterop;
/// <summary>
/// Resolves the NDI native library (<c>Processing.NDI.Lib.x64.dll</c>) from the
/// NDI Runtime install directory exposed via <c>NDI_RUNTIME_DIR_V&lt;n&gt;</c>
/// environment variables.
///
/// The NDI installer creates these env vars but does not always add the runtime
/// directory to <c>PATH</c>, so a default <c>[DllImport("Processing.NDI.Lib.x64")]</c>
/// can fail with <c>0x8007007E</c> ("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 <see cref="NdiNative"/>,
/// 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
/// <c>[SupportedOSPlatform("windows")]</c> and won't fire there in any case.
/// </summary>
internal static class NdiNativeLibraryResolver
{
private const string NdiX64LibraryName = "Processing.NDI.Lib.x64";
private const string NdiX64LibraryFile = "Processing.NDI.Lib.x64.dll";
/// <summary>
/// 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.
/// </summary>
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;
}
}

View file

@ -12,9 +12,14 @@ public static class NdiVersion
public const string SdkFamily = "NDI 6"; public const string SdkFamily = "NDI 6";
/// <summary> /// <summary>
/// Prefix of the runtime version string we expect (NDI runtime reports e.g. /// Prefix of the runtime version banner we expect. The shipping NDI 6 runtime
/// "NDI SDK for Windows v6.0.1.0"). Any major change of the leading "v6" is treated /// reports its version as a build banner of the form
/// as a mismatch. /// "NDI SDK WIN64 13:07:00 Jun 2 2025 6.2.0.3"
/// where the architecture token ("WIN64" / "WIN32") is stable across patch
/// releases and confirms we loaded the binary the engine P/Invokes against
/// (Processing.NDI.Lib.x64.dll). The trailing four-part token is the SDK's
/// numeric version. The probe checks this prefix; a stricter probe could
/// additionally enforce the leading "6." in the trailing version token.
/// </summary> /// </summary>
public const string ExpectedRuntimeVersionPrefix = "NDI SDK for Windows v6"; public const string ExpectedRuntimeVersionPrefix = "NDI SDK WIN64";
} }