summaryrefslogtreecommitdiffstats
path: root/cplugins/gtk/mpv_gtk_helper.inc
diff options
context:
space:
mode:
Diffstat (limited to 'cplugins/gtk/mpv_gtk_helper.inc')
-rw-r--r--cplugins/gtk/mpv_gtk_helper.inc189
1 files changed, 189 insertions, 0 deletions
diff --git a/cplugins/gtk/mpv_gtk_helper.inc b/cplugins/gtk/mpv_gtk_helper.inc
new file mode 100644
index 0000000..ca637ac
--- /dev/null
+++ b/cplugins/gtk/mpv_gtk_helper.inc
@@ -0,0 +1,189 @@
+/*
+ * This is a helper for running GTK UIs within a mpv C plugin. This helper
+ * tries to take care of initializing GTK and running GTK code in a way
+ * that won't conflict between multiple plugins, or if this runs within
+ * libmpv with an existing GTK host.
+ *
+ * While this "works", there are unsolved theoretical race conditions. Most
+ * likely they will remain theoretical.
+ *
+ * How to use:
+ *
+ * - call mpv_gtk_helper_run() in mpv_open_cplugin()
+ * - create your GUI in the callback you pass to mpv_gtk_helper_run()
+ * - once your GUI is built, you can return from the function - at this
+ * point, a GTK mainloop will run, your GUI should appear, and the GTK
+ * signals you connected should be working
+ * - you need to keep mpv_gtk_helper_context around
+ * - you can use mpv_gtk_helper_context.mpv to access libmpv functions
+ * - once you're done or if you get MPV_EVENT_SHUTDOWN, you must call
+ * mpv_gtk_helper_context_destroy()
+ * - this will destroy both the mpv_handle and mpv_gtk_helper_context
+ * - it will most likely also destroy the GTK UI
+ *
+ * Caveats:
+ *
+ * - some race conditions
+ * - mpv_gtk_helper_context_destroy()
+ * - gtk_main_quit() will not work as expected
+ * - deinitialization does not happen with GTK's agreement - instead, mpv
+ * will exit from main(), which kills the GTK thread without the chance
+ * to run additional deallocation and so on
+ *
+ * Additional build flags:
+ *
+ * `pkg-config --cflags --libs gtk+-3.0 x11` -pthread
+ *
+ * License: anything you like as long as you won't sue me
+ */
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <pthread.h>
+
+#include <mpv/client.h>
+#include <gtk/gtk.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+typedef struct mpv_gtk_helper_context mpv_gtk_helper_context;
+
+typedef void (*mpv_gtk_helper_user_fn)(mpv_gtk_helper_context *ctx);
+
+static int mpv_gtk_helper_run(mpv_handle *mpv, mpv_gtk_helper_user_fn fn);
+static void mpv_gtk_helper_context_destroy(mpv_gtk_helper_context *ctx);
+
+struct mpv_gtk_helper_context {
+ mpv_handle *mpv;
+
+ // Internal stuff.
+ mpv_gtk_helper_user_fn user_fn;
+ int return_value;
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ int running;
+};
+
+// Internal.
+static gboolean mpv_gtk_helper_on_main_thread(void *data)
+{
+ mpv_gtk_helper_context *ctx = data;
+ ctx->user_fn(ctx);
+ return FALSE;
+}
+
+// Internal.
+// If GTK is not running yet, this thread will be used as GTK's "main thread".
+// If GTK is already running, we exit the thread immediately after dispatching
+// the user code.
+// This is done on a separate thread because if we call gtk_main(), we want it
+// to be able to run other GTK plugins even when the current plugin exits and
+// terminates.
+static void *mpv_gtk_helper_thread(void *data)
+{
+ mpv_gtk_helper_context *ctx = data;
+
+ pthread_detach(pthread_self());
+
+ GMainContext *main_context = g_main_context_default();
+
+ if (g_main_context_acquire(main_context)) {
+ // If we can get here, it means no GTK mainloop is running (as
+ // gtk_main() always uses the default GMainContext. So we run GTK
+ // in this thread.
+ // Keep in mind that g_main_context_acquire() records our thread's
+ // handle as owner, so this must be run in a dedicated thread.
+ // There is a subtle race condition:
+ // gtk_main() implicitly acquires/releases the main context by
+ // calling g_main_loop_run(), but the code outside of this call
+ // is unprotected - our own gtk_main() call can step over it.
+ // => undefined behavior
+
+ gtk_disable_setlocale();
+
+ // We don't have these anymore, so make something up.
+ int argc = 1;
+ char **argv = (char*[]){"dummy", NULL};
+
+ if (!gtk_init_check(&argc, &argv)) {
+ ctx->return_value = -1;
+ mpv_gtk_helper_context_destroy(ctx);
+ return NULL;
+ }
+
+ ctx->user_fn(ctx);
+
+ gtk_main();
+
+ g_main_context_release(main_context);
+ } else {
+ // A GTK mainloop is running - run the plugin code on it.
+ // There is a subtle race condition:
+ // If we fail to g_main_context_acquire(), it could still happen
+ // that the owner releases the GMainLoop before we get to make
+ // it run something with g_main_context_invoke().
+ // => callback is enqueued but never called, the plugin won't "start"
+ // and mpv will get stuck waiting for it
+
+ g_main_context_invoke(main_context, mpv_gtk_helper_on_main_thread, ctx);
+ }
+
+ return NULL;
+}
+
+// Run the helper. This function will call fn() on the right thread after
+// doing basic GTK initialization. It will block until the user calls
+// mpv_gtk_helper_context_destroy().
+static int mpv_gtk_helper_run(mpv_handle *mpv, mpv_gtk_helper_user_fn fn)
+{
+ // Without this GTK's xlib use will conflict with mpv's.
+ // There is a subtle race condition:
+ // The function is implemented in an unsafe way. It checks for a global
+ // flag whether it's initialized, instead of using pthread_once.
+ // => undefined behavior
+ XInitThreads();
+
+ // Make mpv think initialization finished (i.e. continue loading).
+ mpv_wait_event(mpv, -1);
+
+ mpv_gtk_helper_context ctx = {
+ .mpv = mpv,
+ .user_fn = fn,
+ .return_value = 0,
+ .running = 1,
+ };
+
+ pthread_mutex_init(&ctx.mutex, NULL);
+ pthread_cond_init(&ctx.cond, NULL);
+
+ pthread_t thread;
+ if (pthread_create(&thread, NULL, mpv_gtk_helper_thread, &ctx)) {
+ ctx.return_value = -1;
+ ctx.running = 0;
+ }
+
+ pthread_mutex_lock(&ctx.mutex);
+ while (ctx.running)
+ pthread_cond_wait(&ctx.cond, &ctx.mutex);
+ pthread_mutex_unlock(&ctx.mutex);
+
+ pthread_mutex_destroy(&ctx.mutex);
+ pthread_cond_destroy(&ctx.cond);
+
+ return ctx.return_value;
+}
+
+// Destroy the context and associated resources (in particular ctx->mpv).
+// Once this function is called, ctx and ctx->mpv must be considered invalid.
+// (Technically, ctx is deallocated lazily, but you can't know when exactly
+// this happens.)
+// Keep in mind that the process might be killed once this is called, because
+// mpv might return from main().
+static void mpv_gtk_helper_context_destroy(mpv_gtk_helper_context *ctx)
+{
+ pthread_mutex_lock(&ctx->mutex);
+ ctx->running = 0;
+ pthread_cond_broadcast(&ctx->cond);
+ pthread_mutex_unlock(&ctx->mutex);
+}