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