summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwm4 <wm4@nowhere>2017-01-14 18:48:53 +0100
committerwm4 <wm4@nowhere>2017-01-14 18:48:53 +0100
commitb622611d62a03653086fb1b496c7d472b665891d (patch)
treefc8395edc159292d95ffb2d31d878620aa33a465
parentf2e24d5f8ab2237341d7435b0ba766e4bbc6bf3f (diff)
downloadmpv-examples-b622611d62a03653086fb1b496c7d472b665891d.tar.bz2
mpv-examples-b622611d62a03653086fb1b496c7d472b665891d.tar.xz
Add GTK demo
-rw-r--r--cplugins/README.md4
-rw-r--r--cplugins/gtk/gtk.c100
-rw-r--r--cplugins/gtk/mpv_gtk_helper.inc189
3 files changed, 293 insertions, 0 deletions
diff --git a/cplugins/README.md b/cplugins/README.md
index 06223e0..e85dfbc 100644
--- a/cplugins/README.md
+++ b/cplugins/README.md
@@ -11,3 +11,7 @@ Very primitive terminal-only example. Shows some most basic API usage.
Very primitive example showing basic API usage.
+### GTK
+
+Demonstrates how to use GTK UI elements within a mpv C plugin. Includes some
+glue code to overcome the hostileness of GTK to embedding (mpv_gtk_helper.inc).
diff --git a/cplugins/gtk/gtk.c b/cplugins/gtk/gtk.c
new file mode 100644
index 0000000..304fc18
--- /dev/null
+++ b/cplugins/gtk/gtk.c
@@ -0,0 +1,100 @@
+// Build with: gcc -o gtk.so gtk.c `pkg-config --cflags mpv` `pkg-config --cflags --libs gtk+-3.0 x11` -pthread -shared -fPIC
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+// For mpv_gtk_helper_run() and mpv_gtk_helper_done(). Also pulls in headers.
+#include "mpv_gtk_helper.inc"
+
+// The following code is partially derived from the GTK tutorial example.
+
+struct plugin_context {
+ mpv_gtk_helper_context *helper;
+ GtkWidget *pbar;
+};
+
+static gboolean delete_event( GtkWidget *widget,
+ GdkEvent *event,
+ gpointer data )
+{
+ // Exit UI.
+
+ struct plugin_context *ctx = data;
+
+ if (ctx->helper) {
+ mpv_gtk_helper_context_destroy(ctx->helper);
+ ctx->helper = NULL;
+ }
+
+ return FALSE;
+}
+
+static gboolean handle_mpv_events(void *data)
+{
+ struct plugin_context *ctx = data;
+
+ if (!ctx->helper)
+ return FALSE;
+
+ while (1) {
+ mpv_event *event = mpv_wait_event(ctx->helper->mpv, 0);
+ if (event->event_id == MPV_EVENT_NONE)
+ break;
+
+ printf("event: %s\n", mpv_event_name(event->event_id));
+
+ if (event->event_id == MPV_EVENT_PROPERTY_CHANGE) {
+ mpv_event_property *prop = event->data;
+ if (prop->format == MPV_FORMAT_INT64)
+ gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ctx->pbar),
+ *(int64_t*)prop->data / 100.0);
+ }
+
+ if (event->event_id == MPV_EVENT_SHUTDOWN) {
+ mpv_gtk_helper_context_destroy(ctx->helper);
+ ctx->helper = NULL;
+ break;
+ }
+ }
+
+ return FALSE;
+}
+
+static void wakeup_mpv(void *data)
+{
+ // wakeup_mpv is called in context of an arbitrary mpv thread.
+ // Run our GUI code on the GTK thread by notifying the mainloop.
+ g_idle_add(handle_mpv_events, data);
+}
+
+static void setup_gtk_stuff(mpv_gtk_helper_context *helper)
+{
+ // Out of severe lazyness, you don't free ctx. This is left as exercise
+ // to the reader.
+ struct plugin_context *ctx = calloc(1, sizeof(*ctx));
+ ctx->helper = helper;
+
+ // Make mpv notify us if there are new events.
+ mpv_set_wakeup_callback(ctx->helper->mpv, wakeup_mpv, ctx);
+
+ mpv_observe_property(ctx->helper->mpv, 0, "percent-pos", MPV_FORMAT_INT64);
+
+ GtkWidget *window;
+
+ window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ g_signal_connect (window, "delete-event",
+ G_CALLBACK (delete_event), ctx);
+ gtk_container_set_border_width (GTK_CONTAINER (window), 10);
+ ctx->pbar = gtk_progress_bar_new ();
+ gtk_container_add (GTK_CONTAINER (window), ctx->pbar);
+ gtk_widget_show (ctx->pbar);
+ gtk_widget_show (window);
+}
+
+int mpv_open_cplugin(mpv_handle *handle)
+{
+ printf("Hello world from C plugin '%s'!\n", mpv_client_name(handle));
+
+ return mpv_gtk_helper_run(handle, &setup_gtk_stuff);
+}
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);
+}