public class MpvPlayer : IDisposable { private nint _mpvContext = nint.Zero; private bool _disposed = false; private MpvWakeupCallback? _wakeupCallback; private MpvOpenglGetProcAddressCallback? _procAddressCallback; private MpvRenderUpdateFn? _renderUpdateCallback; private AutoResetEvent _eventSignal = new(false); private Task? _backgroundWorkerTask = null; private CancellationTokenSource _backgroundWorkerCancellationTokenSource = new(); internal event Action? OnRenderRequested; internal GlInterface? _glInterface = null; internal MediaControl? _mediaControl = null; internal nint _mpvRenderContext = nint.Zero; internal ConcurrentQueue _eventQueue = new(); private readonly Dictionary _mpvPropertyChangeEvents = new(); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } internal void Initialise() { if (_glInterface is null) { throw new Exception("OpenGL interface was null. Did you bind MpvPlayer to a MediaControl?"); } var mpv = mpv_create(); _mpvContext = mpv; if (mpv == nint.Zero) { throw new Exception("Failed to create mpv context"); } mpv_set_option_string(mpv, "vo", "libmpv"); if (mpv_initialize(mpv) < 0) { Console.WriteLine("MPV failed to init"); } mpv_request_log_messages(mpv, "debug"); _procAddressCallback = GetProcAddress; var initParams = new MpvOpenglInitParams { get_proc_address = Marshal.GetFunctionPointerForDelegate(_procAddressCallback), get_proc_address_ctx = nint.Zero, }; var enableAdvancedControl = 1; byte[] managedParamApiType = Encoding.UTF8.GetBytes("opengl" + "\0"); unsafe { fixed (byte* paramApiType = managedParamApiType) { MpvRenderParam[] renderParams = { new(){ type = mpv_render_param_type.MPV_RENDER_PARAM_API_TYPE, data = (void*)paramApiType, }, new() { type = mpv_render_param_type.MPV_RENDER_PARAM_OPENGL_INIT_PARAMS , data = &initParams }, new() { type = mpv_render_param_type.MPV_RENDER_PARAM_ADVANCED_CONTROL , data = &enableAdvancedControl }, new() }; fixed (MpvRenderParam* ParamPtr = &renderParams[0]) { nint mpv_gl; int status = mpv_render_context_create( out mpv_gl, mpv, ParamPtr ); _mpvRenderContext = mpv_gl; _wakeupCallback = MpvEvent; _renderUpdateCallback = MpvRenderUpdate; mpv_set_wakeup_callback(mpv, Marshal.GetFunctionPointerForDelegate(_wakeupCallback), nint.Zero); mpv_render_context_set_update_callback(mpv_gl, Marshal.GetFunctionPointerForDelegate(_renderUpdateCallback), nint.Zero); } } } //Start the background worker _backgroundWorkerTask = Task.Run(() => BackgroundWorker(_backgroundWorkerCancellationTokenSource.Token)); } private void MpvEvent(nint data) { Dispatcher.UIThread.Post(() => { _eventSignal.Set(); _eventQueue.Enqueue(CustomEventType.Wakeup); }); } private void MpvRenderUpdate(nint data) { Dispatcher.UIThread.Post(() => { _eventSignal.Set(); _eventQueue.Enqueue(CustomEventType.Render); }); } //marshal a command to mpv public void MpvCommand(string[] command) { Dispatcher.UIThread.Post(() => { if (_mpvContext.isNullPtr()) throw new Exception("Mpv context not properly initialised."); nint[] argPtrs = new nint[command.Length + 1]; for (int i = 0; i < command.Length; i++) { argPtrs[i] = Marshal.StringToHGlobalAnsi(command[i]); } argPtrs[command.Length] = nint.Zero; nint argsPtr = Marshal.AllocHGlobal(nint.Size * argPtrs.Length); Marshal.Copy(argPtrs, 0, argsPtr, argPtrs.Length); int result = mpv_command(_mpvContext, argsPtr); for (int i = 0; i < command.Length; i++) { if (argPtrs[i] != nint.Zero) Marshal.FreeHGlobal(argPtrs[i]); } Marshal.FreeHGlobal(argsPtr); }); } public void RegisterEvent(string property, MpvFormat format) { if (_mpvContext.isNullPtr()) throw new Exception("Mpv context not properly initialised."); if (format != MpvFormat.MPV_FORMAT_DOUBLE && format != MpvFormat.MPV_FORMAT_FLAG && format != MpvFormat.MPV_FORMAT_INT64) throw new System.NotImplementedException("This format has not implemented"); if (_mpvPropertyChangeEvents.ContainsKey(property)) throw new InvalidOperationException("Event with this property name is already registered."); mpv_observe_property(_mpvContext, 0, property, format); _mpvPropertyChangeEvents[property] = (new EventSource(), format); } public EventSource GetEvent(string property) { if (_mpvPropertyChangeEvents.TryGetValue(property, out var evt) && evt is (EventSource ev, MpvFormat format) data) { return ev; } else throw new Exception("Event with this property name is not registered"); } public T? MpvGetProperty(string property, MpvFormat format) { if (_mpvContext.isNullPtr()) throw new Exception("Mpv context not properly initialised."); if (typeof(T) == typeof(string) && format == MpvFormat.MPV_FORMAT_STRING) { return (T?)(object?)MpvGetStringProperty(property); } var resultPtr = Marshal.AllocHGlobal(Marshal.SizeOf()); var ret = mpv_get_property(_mpvContext, property, format, resultPtr); if (ret < 0) { Marshal.FreeHGlobal(resultPtr); return default(T); } var result = Marshal.PtrToStructure(resultPtr); Marshal.FreeHGlobal(resultPtr); return result; } private string? MpvGetStringProperty(string property) { if (_mpvContext.isNullPtr()) throw new Exception("Mpv context not properly initialised."); var resultPtr = Marshal.AllocHGlobal(nint.Size); var ret = mpv_get_property(_mpvContext, property, MpvFormat.MPV_FORMAT_STRING, resultPtr); if (ret < 0) { Marshal.FreeHGlobal(resultPtr); return null; } var stringPtr = Marshal.ReadIntPtr(resultPtr); var result = Marshal.PtrToStringUTF8(stringPtr); mpv_free(stringPtr); Marshal.FreeHGlobal(resultPtr); return result; } private nint GetProcAddress(nint fn_ctx, [MarshalAs(UnmanagedType.LPStr)] string name) { //this should not be null return _glInterface!.GetProcAddress(name); } private void BackgroundWorker(CancellationToken ctx) { while (true) { bool redraw = false; _eventSignal.WaitOne(); if (ctx.IsCancellationRequested) return; CustomEventType _ev; while (_eventQueue.TryDequeue(out _ev)) { if (ctx.IsCancellationRequested) return; switch (_ev) { case CustomEventType.Wakeup: Dispatcher.UIThread.Post(() => { while (true) { if (_mpvContext.isNullPtr()) continue; nint evPtr = mpv_wait_event(_mpvContext, 0.0); if (evPtr == nint.Zero) break; MpvEvent ev = Marshal.PtrToStructure(evPtr); if (ev.event_id == MpvEventId.MPV_EVENT_NONE) break; if (ev.event_id == MpvEventId.MPV_EVENT_PROPERTY_CHANGE) { var prop = Marshal.PtrToStructure(ev.data); var name = Marshal.PtrToStringAnsi(prop.name); if (name is null) continue; if (prop.data == nint.Zero) continue; if (_mpvPropertyChangeEvents.TryGetValue(name, out (object ev, MpvFormat format) _data)) { switch (_data.format) { case MpvFormat.MPV_FORMAT_DOUBLE: { if (_data.ev is EventSource eventSource) { var value = Marshal.PtrToStructure(prop.data); eventSource.Raise(this, value); } break; } case MpvFormat.MPV_FORMAT_FLAG: { if (_data.ev is EventSource eventSource) { var value = Marshal.PtrToStructure(prop.data); eventSource.Raise(this, value); } break; } } } } } }); break; case CustomEventType.Render: if (_mpvRenderContext.isNullPtr()) continue; var flags = mpv_render_context_update(_mpvRenderContext); if ((flags & (1 << 0)) != 0) { redraw = true; } break; } } if (redraw) { if (ctx.IsCancellationRequested) return; //trigger render here Dispatcher.UIThread.Post(() => { _mediaControl?.TriggerRender(); }); } } } public void StartPlayback(string source) { string[] command = { "loadfile", source }; MpvCommand(command); } public void TogglePlayPause() { string[] command = { "cycle", "pause" }; MpvCommand(command); } public void SeekTo(double ms) { string[] command = { "seek", $"{ms}", "absolute" }; MpvCommand(command); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { } mpv_set_wakeup_callback(_mpvContext, nint.Zero, nint.Zero); if (!_mpvRenderContext.isNullPtr()) mpv_render_context_set_update_callback(_mpvRenderContext, nint.Zero, nint.Zero); foreach (var p in _mpvPropertyChangeEvents.Values) { if (p.Item1 is IDisposable disposable) disposable.Dispose(); } _mpvPropertyChangeEvents.Clear(); mpv_unobserve_property(_mpvContext, 0); _backgroundWorkerCancellationTokenSource.Cancel(); _eventSignal.Set(); _backgroundWorkerTask?.Wait(300); _mpvPropertyChangeEvents.Clear(); if (!_mpvRenderContext.isNullPtr()) { mpv_render_context_free(_mpvRenderContext); _mpvRenderContext = nint.Zero; } if (!_mpvContext.isNullPtr()) { mpv_destroy(_mpvContext); _mpvContext = nint.Zero; } _disposed = true; } } }