From b622611d62a03653086fb1b496c7d472b665891d Mon Sep 17 00:00:00 2001 From: wm4 Date: Sat, 14 Jan 2017 18:48:53 +0100 Subject: Add GTK demo --- cplugins/README.md | 4 + cplugins/gtk/gtk.c | 100 +++++++++++++++++++++ cplugins/gtk/mpv_gtk_helper.inc | 189 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 cplugins/gtk/gtk.c create mode 100644 cplugins/gtk/mpv_gtk_helper.inc 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 +#include +#include + +// 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 +#include +#include + +#include +#include + +#include +#include + +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); +} -- cgit v1.2.3