diff --git a/source/LibRender2/LibRender2.csproj b/source/LibRender2/LibRender2.csproj
index dcea6d70e..01af987e8 100644
--- a/source/LibRender2/LibRender2.csproj
+++ b/source/LibRender2/LibRender2.csproj
@@ -122,6 +122,7 @@
+
diff --git a/source/LibRender2/Textures/TextureManager.cs b/source/LibRender2/Textures/TextureManager.cs
index a7eb1518a..07a547b50 100644
--- a/source/LibRender2/Textures/TextureManager.cs
+++ b/source/LibRender2/Textures/TextureManager.cs
@@ -58,12 +58,64 @@ public bool RegisterTexture(string path, out Texture handle)
/// Whether registering the texture was successful.
public bool RegisterTexture(string path, TextureParameters parameters, out Texture handle)
{
- if (!File.Exists(path))
+ if (string.IsNullOrEmpty(path))
+ {
+ handle = null;
+ return false;
+ }
+
+ string cleanPath = path;
+ string queryString = "";
+ int qIndex = path.IndexOf('?');
+ if (qIndex >= 0)
+ {
+ cleanPath = path.Substring(0, qIndex);
+ queryString = path.Substring(qIndex + 1);
+ }
+
+ // Block internet URLs and network shares to prevent security flaws
+ if (cleanPath.Contains("://") || cleanPath.StartsWith("\\\\") || cleanPath.StartsWith("//"))
+ {
+ handle = null;
+ return false;
+ }
+
+ if (!File.Exists(cleanPath))
{
// shouldn't happen, but handle gracefully
handle = null;
return false;
}
+
+ string ext = System.IO.Path.GetExtension(cleanPath).ToLowerInvariant();
+ if (ext == ".mp4" || ext == ".mkv" || ext == ".webm" || ext == ".avi")
+ {
+ for (int i = 0; i < RegisteredTexturesCount; i++)
+ {
+ if (RegisteredTextures[i] != null && RegisteredTextures[i].VideoContext != null)
+ {
+ VideoTexture videoTex = RegisteredTextures[i].VideoContext as VideoTexture;
+ if (videoTex != null && videoTex.VideoPath.Equals(cleanPath, StringComparison.InvariantCultureIgnoreCase))
+ {
+ handle = RegisteredTextures[i];
+ return true;
+ }
+ }
+ }
+
+ int width = 512;
+ int height = 512;
+ byte[] dummyBytes = new byte[width * height * 4];
+ Texture dummyTexture = new Texture(width, height, PixelFormat.RGBAlpha, dummyBytes, null);
+ dummyTexture.VideoContext = new VideoTexture(cleanPath, width, height, queryString);
+ dummyTexture.AvailableToUnload = true;
+
+ int videoIdx = GetNextFreeTexture();
+ RegisteredTextures[videoIdx] = dummyTexture;
+ RegisteredTexturesCount++;
+ handle = RegisteredTextures[videoIdx];
+ return true;
+ }
/* BUG:
* Attempt to delete null texture handles from the end of the array
* These sometimes seem to end up there
@@ -203,6 +255,45 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr
{
return false;
}
+
+ if (handle.VideoContext != null)
+ {
+ VideoTexture video = handle.VideoContext as VideoTexture;
+ if (video != null)
+ {
+ if (!handle.OpenGlTextures[(int)wrap].Valid)
+ {
+ int[] names = new int[1];
+ GL.GenTextures(1, names);
+ GL.BindTexture(TextureTarget.Texture2D, names[0]);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (float)TextureMinFilter.Linear);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (float)TextureMagFilter.Linear);
+ if ((wrap & OpenGlTextureWrapMode.RepeatClamp) != 0)
+ {
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (float)TextureWrapMode.Repeat);
+ }
+ else
+ {
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (float)TextureWrapMode.ClampToEdge);
+ }
+
+ if ((wrap & OpenGlTextureWrapMode.ClampRepeat) != 0)
+ {
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (float)TextureWrapMode.Repeat);
+ }
+ else
+ {
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (float)TextureWrapMode.ClampToEdge);
+ }
+ GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, video.Width, video.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
+ handle.OpenGlTextures[(int)wrap].Name = names[0];
+ handle.OpenGlTextures[(int)wrap].Valid = true;
+ video.Initialize();
+ }
+ video.Render(handle.OpenGlTextures[(int)wrap].Name, currentTicks);
+ return true;
+ }
+ }
if (handle.MultipleFrames)
{
@@ -479,6 +570,13 @@ public static void UnloadTexture(ref Texture handle)
return;
}
+ if (handle.VideoContext != null)
+ {
+ IDisposable video = handle.VideoContext as IDisposable;
+ video?.Dispose();
+ handle.VideoContext = null;
+ }
+
if (handle.MultipleFrames)
{
for (int i = 0; i < handle.TotalFrames; i++)
@@ -587,9 +685,20 @@ public void UnloadUnusedTextures(double TimeElapsed)
{
for (int i = 0; i < RegisteredTextures.Length; i++)
{
- if (RegisteredTextures[i] != null && RegisteredTextures[i].AvailableToUnload && (CPreciseTimer.GetClockTicks() - RegisteredTextures[i].LastAccess) > 20000)
+ if (RegisteredTextures[i] != null)
{
- UnloadTexture(ref RegisteredTextures[i]);
+ if (RegisteredTextures[i].VideoContext != null)
+ {
+ VideoTexture video = RegisteredTextures[i].VideoContext as VideoTexture;
+ if (video != null && (CPreciseTimer.GetClockTicks() - RegisteredTextures[i].LastAccess) > 100)
+ {
+ video.Pause();
+ }
+ }
+ if (RegisteredTextures[i].AvailableToUnload && (CPreciseTimer.GetClockTicks() - RegisteredTextures[i].LastAccess) > 20000)
+ {
+ UnloadTexture(ref RegisteredTextures[i]);
+ }
}
}
}
diff --git a/source/LibRender2/Textures/VideoTexture.cs b/source/LibRender2/Textures/VideoTexture.cs
new file mode 100644
index 000000000..81253343d
--- /dev/null
+++ b/source/LibRender2/Textures/VideoTexture.cs
@@ -0,0 +1,357 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using OpenBveApi.Textures;
+using OpenTK.Graphics.OpenGL;
+
+namespace LibRender2.Textures
+{
+ public class VideoTexture : IDisposable
+ {
+ public string VideoPath { get; private set; }
+ public int Width { get; private set; }
+ public int Height { get; private set; }
+ public int TextureId { get; private set; }
+
+ private IntPtr mpvHandle;
+ private IntPtr renderContext;
+ private int fboId;
+ private bool initialized;
+ private bool shouldResize;
+ private bool resolutionResolved;
+ private VideoApi.GetProcAddressCallback getProcAddressDelegate;
+
+ private string loopOption = "inf";
+ private bool isPaused;
+
+ public void Pause()
+ {
+ if (initialized && !isPaused)
+ {
+ VideoApi.mpv_set_option_string(mpvHandle, "pause", "yes");
+ isPaused = true;
+ }
+ }
+
+ public void Resume()
+ {
+ if (initialized && isPaused)
+ {
+ VideoApi.mpv_set_option_string(mpvHandle, "pause", "no");
+ isPaused = false;
+ }
+ }
+
+ public VideoTexture(string videoPath, int width = 512, int height = 512, string queryString = "")
+ {
+ VideoPath = videoPath;
+ Width = width;
+ Height = height;
+
+ if (!string.IsNullOrEmpty(queryString))
+ {
+ string[] pairs = queryString.Split('&');
+ foreach (string pair in pairs)
+ {
+ string[] kv = pair.Split('=');
+ if (kv.Length == 2)
+ {
+ string key = kv[0].Trim().ToLowerInvariant();
+ string val = kv[1].Trim().ToLowerInvariant();
+ if (key == "loop")
+ {
+ if (val == "no" || val == "false" || val == "0")
+ {
+ loopOption = "no";
+ }
+ else
+ {
+ loopOption = val;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public void Initialize()
+ {
+ if (initialized) return;
+
+ try
+ {
+ mpvHandle = VideoApi.mpv_create();
+ if (mpvHandle == IntPtr.Zero) return;
+
+ // Loop video and optimize caching
+ VideoApi.mpv_set_option_string(mpvHandle, "loop-file", loopOption);
+ VideoApi.mpv_set_option_string(mpvHandle, "keep-open", "yes");
+ VideoApi.mpv_set_option_string(mpvHandle, "hwdec", "auto-safe");
+ VideoApi.mpv_set_option_string(mpvHandle, "vo", "libmpv");
+ VideoApi.mpv_set_option_string(mpvHandle, "ytdl", "no");
+ VideoApi.mpv_set_option_string(mpvHandle, "load-scripts", "no");
+ VideoApi.mpv_set_option_string(mpvHandle, "aid", "no");
+ VideoApi.mpv_set_option_string(mpvHandle, "vd-lavc-skiploopfilter", "all");
+ VideoApi.mpv_set_option_string(mpvHandle, "vd-lavc-fast", "yes");
+ VideoApi.mpv_set_option_string(mpvHandle, "vd-lavc-threads", "2");
+ VideoApi.mpv_set_option_string(mpvHandle, "demuxer-max-bytes", "10485760");
+ VideoApi.mpv_set_option_string(mpvHandle, "demuxer-max-back-bytes", "0");
+
+ if (VideoApi.mpv_initialize(mpvHandle) < 0)
+ {
+ Dispose();
+ return;
+ }
+
+ // Setup OpenGL context sharing
+ getProcAddressDelegate = VideoApi.GetProcAddressOpenGL;
+ IntPtr getProcAddressPtr = Marshal.GetFunctionPointerForDelegate(getProcAddressDelegate);
+
+ VideoApi.mpv_opengl_init_params glInitParams = new VideoApi.mpv_opengl_init_params
+ {
+ get_proc_address = getProcAddressPtr,
+ get_proc_address_ctx = IntPtr.Zero,
+ extra_fields = IntPtr.Zero
+ };
+
+ IntPtr glInitParamsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(glInitParams));
+ Marshal.StructureToPtr(glInitParams, glInitParamsPtr, false);
+
+ VideoApi.mpv_render_param[] renderParams = new VideoApi.mpv_render_param[]
+ {
+ new VideoApi.mpv_render_param { type = VideoApi.MPV_RENDER_PARAM_API_TYPE, data = Marshal.StringToHGlobalAnsi(VideoApi.MPV_RENDER_API_TYPE_OPENGL) },
+ new VideoApi.mpv_render_param { type = VideoApi.MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, data = glInitParamsPtr },
+ new VideoApi.mpv_render_param { type = VideoApi.MPV_RENDER_PARAM_TYPE_INVALID, data = IntPtr.Zero }
+ };
+
+ if (VideoApi.mpv_render_context_create(out renderContext, mpvHandle, renderParams) < 0)
+ {
+ Marshal.FreeHGlobal(renderParams[0].data);
+ Marshal.FreeHGlobal(glInitParamsPtr);
+ Dispose();
+ return;
+ }
+
+ Marshal.FreeHGlobal(renderParams[0].data);
+ Marshal.FreeHGlobal(glInitParamsPtr);
+
+ // Load video
+ string commandString = "loadfile";
+ IntPtr cmdStringPtr = Marshal.StringToHGlobalAnsi(commandString);
+ IntPtr pathPtr = Marshal.StringToHGlobalAnsi(VideoPath);
+ IntPtr[] args = new IntPtr[] { cmdStringPtr, pathPtr, IntPtr.Zero };
+ VideoApi.mpv_command(mpvHandle, args);
+ Marshal.FreeHGlobal(cmdStringPtr);
+ Marshal.FreeHGlobal(pathPtr);
+
+ initialized = true;
+ }
+ catch
+ {
+ Dispose();
+ }
+ }
+
+ private void UpdateResolution()
+ {
+ if (mpvHandle == IntPtr.Zero || resolutionResolved) return;
+
+ IntPtr wPtr = VideoApi.mpv_get_property_string(mpvHandle, "video-params/w");
+ IntPtr hPtr = VideoApi.mpv_get_property_string(mpvHandle, "video-params/h");
+
+ if (wPtr != IntPtr.Zero && hPtr != IntPtr.Zero)
+ {
+ string wStr = Marshal.PtrToStringAnsi(wPtr);
+ string hStr = Marshal.PtrToStringAnsi(hPtr);
+
+ if (int.TryParse(wStr, out int w) && int.TryParse(hStr, out int h))
+ {
+ if (w > 0 && h > 0)
+ {
+ Width = w;
+ Height = h;
+ shouldResize = true;
+ resolutionResolved = true;
+ }
+ }
+ }
+
+ if (wPtr != IntPtr.Zero) VideoApi.mpv_free(wPtr);
+ if (hPtr != IntPtr.Zero) VideoApi.mpv_free(hPtr);
+ }
+
+ private int lastRenderTicks = -1;
+
+ public void Render(int glTextureId, int currentTicks)
+ {
+ if (!initialized) return;
+ Resume();
+
+ // Limit video rendering to ~30 FPS (33ms) to prevent FPS drops
+ int elapsed = currentTicks - lastRenderTicks;
+ if (lastRenderTicks != -1 && elapsed >= 0 && elapsed < 33) return;
+
+ // Check if new frame is actually available to render
+ ulong flags = VideoApi.mpv_render_context_update(renderContext);
+ if ((flags & 1) == 0) // MPV_RENDER_UPDATE_FRAME = 1 << 0
+ {
+ return;
+ }
+ lastRenderTicks = currentTicks;
+
+ try
+ {
+ TextureId = glTextureId;
+
+ UpdateResolution();
+
+ if (shouldResize)
+ {
+ GL.BindTexture(TextureTarget.Texture2D, glTextureId);
+ GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, Width, Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
+ shouldResize = false;
+ }
+
+ if (fboId == 0)
+ {
+ GL.GenFramebuffers(1, out fboId);
+ }
+
+ // Backup OpenGL state
+ int prevFbo;
+ GL.GetInteger(GetPName.FramebufferBinding, out prevFbo);
+
+ int prevVAO, prevVBO, prevEBO, prevProgram, prevTexture, prevActiveTexture;
+ GL.GetInteger(GetPName.VertexArrayBinding, out prevVAO);
+ GL.GetInteger(GetPName.ArrayBufferBinding, out prevVBO);
+ GL.GetInteger(GetPName.ElementArrayBufferBinding, out prevEBO);
+ GL.GetInteger(GetPName.CurrentProgram, out prevProgram);
+ GL.GetInteger(GetPName.TextureBinding2D, out prevTexture);
+ GL.GetInteger(GetPName.ActiveTexture, out prevActiveTexture);
+
+ int[] prevViewport = new int[4];
+ GL.GetInteger(GetPName.Viewport, prevViewport);
+
+ bool prevScissor = GL.IsEnabled(EnableCap.ScissorTest);
+ int[] prevScissorBox = new int[4];
+ GL.GetInteger(GetPName.ScissorBox, prevScissorBox);
+
+ bool prevDepth = GL.IsEnabled(EnableCap.DepthTest);
+ bool prevBlend = GL.IsEnabled(EnableCap.Blend);
+ bool prevCull = GL.IsEnabled(EnableCap.CullFace);
+
+ // Backup write masks
+ bool[] prevColorMask = new bool[4];
+ GL.GetBoolean(GetPName.ColorWritemask, prevColorMask);
+ bool prevDepthMask;
+ GL.GetBoolean(GetPName.DepthWritemask, out prevDepthMask);
+
+ // Backup stencil test
+ bool prevStencil = GL.IsEnabled(EnableCap.StencilTest);
+
+ // Backup blend equation and functions
+ int prevBlendSrcRgb, prevBlendDstRgb, prevBlendSrcAlpha, prevBlendDstAlpha;
+ GL.GetInteger(GetPName.BlendSrcRgb, out prevBlendSrcRgb);
+ GL.GetInteger(GetPName.BlendDstRgb, out prevBlendDstRgb);
+ GL.GetInteger(GetPName.BlendSrcAlpha, out prevBlendSrcAlpha);
+ GL.GetInteger(GetPName.BlendDstAlpha, out prevBlendDstAlpha);
+ int prevBlendEqRgb, prevBlendEqAlpha;
+ GL.GetInteger(GetPName.BlendEquationRgb, out prevBlendEqRgb);
+ GL.GetInteger(GetPName.BlendEquationAlpha, out prevBlendEqAlpha);
+
+ // Backup clear color
+ float[] prevClearColor = new float[4];
+ GL.GetFloat(GetPName.ColorClearValue, prevClearColor);
+
+ // Bind FBO and texture
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId);
+ GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, TextureId, 0);
+
+ // Set viewport for FBO size
+ GL.Viewport(0, 0, Width, Height);
+ GL.Disable(EnableCap.ScissorTest);
+
+ mpv_opengl_fbo fboParam = new mpv_opengl_fbo
+ {
+ fbo = fboId,
+ w = Width,
+ h = Height,
+ internal_format = 0x8058 // GL_RGBA8
+ };
+
+ IntPtr fboParamPtr = Marshal.AllocHGlobal(Marshal.SizeOf(fboParam));
+ Marshal.StructureToPtr(fboParam, fboParamPtr, false);
+
+ VideoApi.mpv_render_param[] renderParams = new VideoApi.mpv_render_param[]
+ {
+ new VideoApi.mpv_render_param { type = 3, data = fboParamPtr }, // MPV_RENDER_PARAM_OPENGL_FBO = 3
+ new VideoApi.mpv_render_param { type = 0, data = IntPtr.Zero }
+ };
+
+ VideoApi.mpv_render_context_render(renderContext, renderParams);
+
+ Marshal.FreeHGlobal(fboParamPtr);
+
+ // Restore OpenGL state
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, prevFbo);
+ GL.UseProgram(prevProgram);
+ GL.BindVertexArray(prevVAO);
+ GL.BindBuffer(BufferTarget.ArrayBuffer, prevVBO);
+ GL.BindBuffer(BufferTarget.ElementArrayBuffer, prevEBO);
+ GL.ActiveTexture((TextureUnit)prevActiveTexture);
+ GL.BindTexture(TextureTarget.Texture2D, prevTexture);
+
+ GL.Viewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]);
+ if (prevScissor) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest);
+ GL.Scissor(prevScissorBox[0], prevScissorBox[1], prevScissorBox[2], prevScissorBox[3]);
+ if (prevDepth) GL.Enable(EnableCap.DepthTest); else GL.Disable(EnableCap.DepthTest);
+ if (prevBlend) GL.Enable(EnableCap.Blend); else GL.Disable(EnableCap.Blend);
+ if (prevCull) GL.Enable(EnableCap.CullFace); else GL.Disable(EnableCap.CullFace);
+
+ GL.ColorMask(prevColorMask[0], prevColorMask[1], prevColorMask[2], prevColorMask[3]);
+ GL.DepthMask(prevDepthMask);
+ if (prevStencil) GL.Enable(EnableCap.StencilTest); else GL.Disable(EnableCap.StencilTest);
+
+ // Safely restore blend functions and equations
+ GL.BlendFuncSeparate((BlendingFactorSrc)prevBlendSrcRgb, (BlendingFactorDest)prevBlendDstRgb, (BlendingFactorSrc)prevBlendSrcAlpha, (BlendingFactorDest)prevBlendDstAlpha);
+ GL.BlendEquationSeparate((BlendEquationMode)prevBlendEqRgb, (BlendEquationMode)prevBlendEqAlpha);
+
+ // Restore clear color
+ GL.ClearColor(prevClearColor[0], prevClearColor[1], prevClearColor[2], prevClearColor[3]);
+ }
+ catch
+ {
+ // Ignore render errors to avoid crash
+ }
+ }
+
+ public void Dispose()
+ {
+ initialized = false;
+ if (renderContext != IntPtr.Zero)
+ {
+ VideoApi.mpv_render_context_free(renderContext);
+ renderContext = IntPtr.Zero;
+ }
+ if (mpvHandle != IntPtr.Zero)
+ {
+ VideoApi.mpv_terminate_destroy(mpvHandle);
+ mpvHandle = IntPtr.Zero;
+ }
+ if (fboId != 0)
+ {
+ GL.DeleteFramebuffers(1, ref fboId);
+ fboId = 0;
+ }
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct mpv_opengl_fbo
+ {
+ public int fbo;
+ public int w;
+ public int h;
+ public int internal_format;
+ }
+}
diff --git a/source/OpenBveApi/OpenBveApi.csproj b/source/OpenBveApi/OpenBveApi.csproj
index c75e05d79..5514098af 100644
--- a/source/OpenBveApi/OpenBveApi.csproj
+++ b/source/OpenBveApi/OpenBveApi.csproj
@@ -302,6 +302,7 @@
+
Textures.cs
diff --git a/source/OpenBveApi/Textures/Textures.cs b/source/OpenBveApi/Textures/Textures.cs
index 6e029c0f5..3e5713d46 100644
--- a/source/OpenBveApi/Textures/Textures.cs
+++ b/source/OpenBveApi/Textures/Textures.cs
@@ -43,6 +43,8 @@ public class Texture
public int TotalFrames;
/// Whether this texture uses the compatible transparency mode (Matches to the nearest color in a restricted palette)
public bool CompatibleTransparencyMode;
+ /// Holds the video context reference
+ public object VideoContext;
/// Gets the color of the given pixel
/// The pixel index
diff --git a/source/OpenBveApi/Textures/VideoApi.cs b/source/OpenBveApi/Textures/VideoApi.cs
new file mode 100644
index 000000000..3cf54b894
--- /dev/null
+++ b/source/OpenBveApi/Textures/VideoApi.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace OpenBveApi.Textures
+{
+ /// Provides API bindings for libmpv video playback.
+ public static class VideoApi
+ {
+ private const string MpvDll = "libmpv-2.dll";
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ private static extern IntPtr LoadLibraryW(string lpLibFileName);
+
+ [DllImport("libdl", EntryPoint = "dlopen")]
+ private static extern IntPtr dlopen(string filename, int flags);
+
+ static VideoApi()
+ {
+ try
+ {
+ string arch = IntPtr.Size == 8 ? "x64" : "x86";
+ string baseDir = AppDomain.CurrentDomain.BaseDirectory;
+ bool isWindows = System.IO.Path.DirectorySeparatorChar == '\\';
+
+ if (isWindows)
+ {
+ string path = System.IO.Path.Combine(baseDir, arch, MpvDll);
+ if (System.IO.File.Exists(path))
+ {
+ LoadLibraryW(path);
+ }
+ }
+ else
+ {
+ // Unix/macOS: Map to .so or .dylib
+ string libName = "libmpv.so";
+ if (System.IO.File.Exists(System.IO.Path.Combine(baseDir, arch, "libmpv.dylib")))
+ {
+ libName = "libmpv.dylib";
+ }
+ string path = System.IO.Path.Combine(baseDir, arch, libName);
+ if (System.IO.File.Exists(path))
+ {
+ dlopen(path, 2); // RTLD_NOW = 2
+ }
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ /// Creates an mpv instance.
+ /// A pointer to the mpv handle.
+ [DllImport(MpvDll, EntryPoint = "mpv_create", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr mpv_create();
+
+ /// Initializes the mpv instance.
+ /// The mpv handle.
+ /// An integer status code.
+ [DllImport(MpvDll, EntryPoint = "mpv_initialize", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int mpv_initialize(IntPtr handle);
+
+ /// Destroys and terminates the mpv instance.
+ /// The mpv handle.
+ [DllImport(MpvDll, EntryPoint = "mpv_terminate_destroy", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void mpv_terminate_destroy(IntPtr handle);
+
+ /// Executes a command on the mpv instance.
+ /// The mpv handle.
+ /// The command arguments.
+ /// An integer status code.
+ [DllImport(MpvDll, EntryPoint = "mpv_command", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int mpv_command(IntPtr handle, IntPtr[] args);
+
+ /// Sets an option string on the mpv instance.
+ /// The mpv handle.
+ /// The option name.
+ /// The option value.
+ /// An integer status code.
+ [DllImport(MpvDll, EntryPoint = "mpv_set_option_string", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int mpv_set_option_string(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value);
+
+ /// Gets a property value from the mpv instance.
+ /// The mpv handle.
+ /// The property name.
+ /// A pointer to the property value string.
+ [DllImport(MpvDll, EntryPoint = "mpv_get_property_string", CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr mpv_get_property_string(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string name);
+
+ /// Frees data allocated by mpv.
+ /// The pointer to the data to free.
+ [DllImport(MpvDll, EntryPoint = "mpv_free", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void mpv_free(IntPtr data);
+
+ /// Invalid render parameter type.
+ public const int MPV_RENDER_PARAM_TYPE_INVALID = 0;
+ /// API type render parameter.
+ public const int MPV_RENDER_PARAM_API_TYPE = 1;
+ /// OpenGL init parameters render parameter.
+ public const int MPV_RENDER_PARAM_OPENGL_INIT_PARAMS = 2;
+
+ /// OpenGL render API type string.
+ public const string MPV_RENDER_API_TYPE_OPENGL = "opengl";
+
+ /// Represents an mpv render parameter.
+ [StructLayout(LayoutKind.Sequential)]
+ public struct mpv_render_param
+ {
+ /// The parameter type.
+ public int type;
+ /// The parameter data pointer.
+ public IntPtr data;
+ }
+
+ /// Represents OpenGL initialization parameters for mpv.
+ [StructLayout(LayoutKind.Sequential)]
+ public struct mpv_opengl_init_params
+ {
+ /// Pointer to the get_proc_address callback.
+ public IntPtr get_proc_address;
+ /// Pointer to the callback context.
+ public IntPtr get_proc_address_ctx;
+ /// Pointer to extra fields.
+ public IntPtr extra_fields;
+ }
+
+ /// Delegate callback to retrieve OpenGL procedure addresses.
+ /// The callback context.
+ /// The procedure name.
+ /// The address of the procedure.
+ public delegate IntPtr GetProcAddressCallback(IntPtr ctx, [MarshalAs(UnmanagedType.LPStr)] string name);
+
+ /// Creates an mpv render context.
+ /// The resulting render context pointer.
+ /// The mpv instance handle.
+ /// The parameter list.
+ /// An integer status code.
+ [DllImport(MpvDll, EntryPoint = "mpv_render_context_create", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int mpv_render_context_create(out IntPtr res, IntPtr mpv, mpv_render_param[] params_list);
+
+ /// Frees the mpv render context.
+ /// The render context pointer.
+ [DllImport(MpvDll, EntryPoint = "mpv_render_context_free", CallingConvention = CallingConvention.Cdecl)]
+ public static extern void mpv_render_context_free(IntPtr ctx);
+
+ /// Renders a frame using the context.
+ /// The render context pointer.
+ /// The render parameter list.
+ /// An integer status code.
+ [DllImport(MpvDll, EntryPoint = "mpv_render_context_render", CallingConvention = CallingConvention.Cdecl)]
+ public static extern int mpv_render_context_render(IntPtr ctx, mpv_render_param[] params_list);
+
+ /// Checks/updates the render context state.
+ /// The render context pointer.
+ /// Flags indicating update status.
+ [DllImport(MpvDll, EntryPoint = "mpv_render_context_update", CallingConvention = CallingConvention.Cdecl)]
+ public static extern ulong mpv_render_context_update(IntPtr ctx);
+
+ // Helper to load DLL on Windows
+ [DllImport("opengl32.dll", CharSet = CharSet.Ansi)]
+ private static extern IntPtr wglGetProcAddress(string name);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
+ private static extern IntPtr GetProcAddress(IntPtr hModule, string name);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
+ private static extern IntPtr GetModuleHandle(string lpModuleName);
+
+ /// Retrieves OpenGL procedure address on Windows.
+ /// The context pointer.
+ /// The procedure name.
+ /// The address of the procedure.
+ public static IntPtr GetProcAddressOpenGL(IntPtr ctx, string name)
+ {
+ try
+ {
+ IntPtr proc = wglGetProcAddress(name);
+ if (proc == IntPtr.Zero)
+ {
+ IntPtr hModule = GetModuleHandle("opengl32.dll");
+ proc = GetProcAddress(hModule, name);
+ }
+ return proc;
+ }
+ catch
+ {
+ return IntPtr.Zero;
+ }
+ }
+ }
+}