diff --git a/src/TeamsISO.Engine.NdiInterop/NdiNative.cs b/src/TeamsISO.Engine.NdiInterop/NdiNative.cs
new file mode 100644
index 0000000..24f61d2
--- /dev/null
+++ b/src/TeamsISO.Engine.NdiInterop/NdiNative.cs
@@ -0,0 +1,154 @@
+using System.Runtime.InteropServices;
+
+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.
+///
+internal static class NdiNative
+{
+ private const string LibName = "Processing.NDI.Lib.x64";
+
+ // ---- Lifecycle ----
+ [DllImport(LibName, EntryPoint = "NDIlib_initialize", CallingConvention = CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static extern bool Initialize();
+
+ [DllImport(LibName, EntryPoint = "NDIlib_destroy", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void Destroy();
+
+ [DllImport(LibName, EntryPoint = "NDIlib_version", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr Version();
+
+ // ---- Find ----
+ [DllImport(LibName, EntryPoint = "NDIlib_find_create_v2", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr FindCreateV2(IntPtr p_create_settings);
+
+ [DllImport(LibName, EntryPoint = "NDIlib_find_destroy", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void FindDestroy(IntPtr p_instance);
+
+ [DllImport(LibName, EntryPoint = "NDIlib_find_get_current_sources", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr FindGetCurrentSources(IntPtr p_instance, out uint p_no_sources);
+
+ // ---- Receive ----
+ [DllImport(LibName, EntryPoint = "NDIlib_recv_create_v3", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr RecvCreateV3(ref RecvCreateV3 p_create_settings);
+
+ [DllImport(LibName, EntryPoint = "NDIlib_recv_destroy", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void RecvDestroy(IntPtr p_instance);
+
+ [DllImport(LibName, EntryPoint = "NDIlib_recv_capture_v3", CallingConvention = CallingConvention.Cdecl)]
+ public static extern FrameType RecvCaptureV3(
+ IntPtr p_instance,
+ out VideoFrameV2 p_video_data,
+ IntPtr p_audio_data,
+ IntPtr p_metadata,
+ uint timeout_in_ms);
+
+ [DllImport(LibName, EntryPoint = "NDIlib_recv_free_video_v2", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void RecvFreeVideoV2(IntPtr p_instance, ref VideoFrameV2 p_video_data);
+
+ // ---- Send ----
+ [DllImport(LibName, EntryPoint = "NDIlib_send_create", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr SendCreate(ref SendCreate p_create_settings);
+
+ [DllImport(LibName, EntryPoint = "NDIlib_send_destroy", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void SendDestroy(IntPtr p_instance);
+
+ [DllImport(LibName, EntryPoint = "NDIlib_send_send_video_v2", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void SendSendVideoV2(IntPtr p_instance, ref VideoFrameV2 p_video_data);
+
+ // ---- Enums ----
+
+ public enum FrameType
+ {
+ None = 0,
+ Video = 1,
+ Audio = 2,
+ Metadata = 3,
+ Error = 4,
+ StatusChange = 100
+ }
+
+ public enum RecvBandwidth
+ {
+ MetadataOnly = -10,
+ AudioOnly = 10,
+ Lowest = 0,
+ Highest = 100
+ }
+
+ public enum RecvColorFormat
+ {
+ BgrxBgra = 0,
+ UyvyBgra = 1,
+ Rgbx_Rgba = 2,
+ UyvyRgba = 3,
+ Fastest = 100,
+ Best = 101
+ }
+
+ public enum FrameFormatType
+ {
+ Progressive = 1,
+ Interleaved = 0,
+ FieldZero = 2,
+ FieldOne = 3,
+ Max = 0x7FFFFFFF
+ }
+
+ public static class FourCC
+ {
+ public const uint UYVY = 0x59565955; // 'U','Y','V','Y'
+ public const uint BGRA = 0x41524742; // 'B','G','R','A'
+ public const uint BGRX = 0x58524742; // 'B','G','R','X'
+ public const uint RGBA = 0x41424752; // 'R','G','B','A'
+ }
+
+ // ---- Structs ----
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct Source
+ {
+ public IntPtr p_ndi_name; // const char*
+ public IntPtr p_url_address; // const char* (union, treat as URL)
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct RecvCreateV3
+ {
+ public Source source_to_connect_to;
+ public RecvColorFormat color_format;
+ public RecvBandwidth bandwidth;
+ [MarshalAs(UnmanagedType.U1)] public bool allow_video_fields;
+ public IntPtr p_ndi_recv_name; // const char*
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct SendCreate
+ {
+ public IntPtr p_ndi_name; // const char*
+ public IntPtr p_groups; // const char*
+ [MarshalAs(UnmanagedType.U1)] public bool clock_video;
+ [MarshalAs(UnmanagedType.U1)] public bool clock_audio;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct VideoFrameV2
+ {
+ public int xres;
+ public int yres;
+ public uint FourCC;
+ public int frame_rate_N;
+ public int frame_rate_D;
+ public float picture_aspect_ratio;
+ public FrameFormatType frame_format_type;
+ public long timecode;
+ public IntPtr p_data;
+ public int line_stride_in_bytes; // (union with data_size_in_bytes)
+ public IntPtr p_metadata;
+ public long timestamp;
+ }
+}
diff --git a/src/TeamsISO.Engine.NdiInterop/NdiPInvokeHandles.cs b/src/TeamsISO.Engine.NdiInterop/NdiPInvokeHandles.cs
new file mode 100644
index 0000000..ae67830
--- /dev/null
+++ b/src/TeamsISO.Engine.NdiInterop/NdiPInvokeHandles.cs
@@ -0,0 +1,61 @@
+using TeamsISO.Engine.Interop;
+
+namespace TeamsISO.Engine.NdiInterop;
+
+internal sealed class NdiPInvokeFindHandle : NdiFindHandle
+{
+ public IntPtr Native { get; private set; }
+
+ public NdiPInvokeFindHandle(IntPtr native) => Native = native;
+
+ public override void Dispose()
+ {
+ if (Native != IntPtr.Zero)
+ {
+ NdiNative.FindDestroy(Native);
+ Native = IntPtr.Zero;
+ }
+ }
+}
+
+internal sealed class NdiPInvokeReceiverHandle : NdiReceiverHandle
+{
+ public IntPtr Native { get; private set; }
+ public string SourceName { get; }
+
+ public NdiPInvokeReceiverHandle(IntPtr native, string sourceName)
+ {
+ Native = native;
+ SourceName = sourceName;
+ }
+
+ public override void Dispose()
+ {
+ if (Native != IntPtr.Zero)
+ {
+ NdiNative.RecvDestroy(Native);
+ Native = IntPtr.Zero;
+ }
+ }
+}
+
+internal sealed class NdiPInvokeSenderHandle : NdiSenderHandle
+{
+ public IntPtr Native { get; private set; }
+ public string OutputName { get; }
+
+ public NdiPInvokeSenderHandle(IntPtr native, string outputName)
+ {
+ Native = native;
+ OutputName = outputName;
+ }
+
+ public override void Dispose()
+ {
+ if (Native != IntPtr.Zero)
+ {
+ NdiNative.SendDestroy(Native);
+ Native = IntPtr.Zero;
+ }
+ }
+}
diff --git a/src/TeamsISO.Engine.NdiInterop/NdiVersion.cs b/src/TeamsISO.Engine.NdiInterop/NdiVersion.cs
new file mode 100644
index 0000000..4d7c896
--- /dev/null
+++ b/src/TeamsISO.Engine.NdiInterop/NdiVersion.cs
@@ -0,0 +1,20 @@
+namespace TeamsISO.Engine.NdiInterop;
+
+///
+/// Constants describing the NDI SDK version this build was compiled against.
+/// The runtime version reported by is compared against
+/// by the engine's runtime probe to detect
+/// installations that pre-date or post-date the SDK headers (per spec ยง6).
+///
+public static class NdiVersion
+{
+ /// The SDK family this build targets (NDI 6).
+ public const string SdkFamily = "NDI 6";
+
+ ///
+ /// Prefix of the runtime version string we expect (NDI runtime reports e.g.
+ /// "NDI SDK for Windows v6.0.1.0"). Any major change of the leading "v6" is treated
+ /// as a mismatch.
+ ///
+ public const string ExpectedRuntimeVersionPrefix = "NDI SDK for Windows v6";
+}