From 580cd1fe2b8731d24446f64d448a6bd1fcabc73e Mon Sep 17 00:00:00 2001 From: Brendan Szymanski Date: Wed, 24 Jun 2026 00:08:46 -0400 Subject: [PATCH] Add mpv_widget C GObject with GtkGLArea rendering --- Sources/CModules/include/mpv_widget.h | 36 +++ Sources/CModules/mpv_widget.c | 429 ++++++++++++++++++++++++++ 2 files changed, 465 insertions(+) create mode 100644 Sources/CModules/include/mpv_widget.h create mode 100644 Sources/CModules/mpv_widget.c diff --git a/Sources/CModules/include/mpv_widget.h b/Sources/CModules/include/mpv_widget.h new file mode 100644 index 0000000..e84a344 --- /dev/null +++ b/Sources/CModules/include/mpv_widget.h @@ -0,0 +1,36 @@ +#ifndef MPV_WIDGET_H +#define MPV_WIDGET_H + +#include + +G_BEGIN_DECLS + +#define MPV_TYPE_WIDGET (mpv_widget_get_type()) +#define MPV_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), MPV_TYPE_WIDGET, MpvWidget)) +#define MPV_IS_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), MPV_TYPE_WIDGET)) + +typedef struct _MpvWidget MpvWidget; +typedef struct _MpvWidgetClass MpvWidgetClass; + +struct _MpvWidgetClass { + GtkWidgetClass parent_class; +}; + +GType mpv_widget_get_type (void); +GtkWidget *mpv_widget_new (void); + +void mpv_widget_load_url (MpvWidget *self, const char *url); +void mpv_widget_play (MpvWidget *self); +void mpv_widget_pause (MpvWidget *self); +void mpv_widget_seek_relative (MpvWidget *self, double offset_seconds); +void mpv_widget_seek_absolute (MpvWidget *self, double position_seconds); +void mpv_widget_set_volume (MpvWidget *self, double volume); + +double mpv_widget_get_position (MpvWidget *self); +double mpv_widget_get_duration (MpvWidget *self); +gboolean mpv_widget_get_paused (MpvWidget *self); +double mpv_widget_get_volume (MpvWidget *self); + +G_END_DECLS + +#endif /* MPV_WIDGET_H */ diff --git a/Sources/CModules/mpv_widget.c b/Sources/CModules/mpv_widget.c new file mode 100644 index 0000000..7a97287 --- /dev/null +++ b/Sources/CModules/mpv_widget.c @@ -0,0 +1,429 @@ +#include "mpv_widget.h" +#include +#include +#include +#include + +struct _MpvWidget { + GtkWidget parent_instance; + GtkWidget *gl_area; + mpv_handle *mpv; + mpv_render_context *mpv_gl; + guint tick_source; + double position; + double duration; + gboolean paused; +}; + +enum { + SIGNAL_POSITION_CHANGED, + SIGNAL_DURATION_CHANGED, + SIGNAL_PLAYBACK_STATE_CHANGED, + SIGNAL_ERROR, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE(MpvWidget, mpv_widget, GTK_TYPE_WIDGET) + +static void +mpv_render_update(void *cb_ctx) +{ + MpvWidget *self = MPV_WIDGET(cb_ctx); + gtk_gl_area_queue_render(GTK_GL_AREA(self->gl_area)); +} + +static void * +mpv_gl_get_proc_address(void *fn_ctx, const char *name) +{ + (void)fn_ctx; + return dlsym(RTLD_DEFAULT, name); +} + +static gboolean mpv_widget_tick(gpointer user_data); + +static void +mpv_widget_on_realize(GtkWidget *widget, gpointer user_data) +{ + MpvWidget *self = MPV_WIDGET(user_data); + + gtk_gl_area_make_current(GTK_GL_AREA(widget)); + + self->mpv = mpv_create(); + if (!self->mpv) + return; + + mpv_set_option_string(self->mpv, "vo", "gpu-api"); + mpv_set_option_string(self->mpv, "hwdec", "auto"); + mpv_set_option_string(self->mpv, "keep-open", "yes"); + mpv_set_option_string(self->mpv, "gpu-context", "drm"); + + if (mpv_initialize(self->mpv) < 0) { + mpv_terminate_destroy(self->mpv); + self->mpv = NULL; + return; + } + + mpv_opengl_init_params gl_init = { + .get_proc_address = mpv_gl_get_proc_address, + .get_proc_address_ctx = NULL, + }; + + mpv_render_param params[] = { + {MPV_RENDER_PARAM_API_TYPE, MPV_RENDER_API_TYPE_OPENGL}, + {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init}, + {0} + }; + + if (mpv_render_context_create(&self->mpv_gl, self->mpv, params) < 0) { + mpv_terminate_destroy(self->mpv); + self->mpv = NULL; + return; + } + + mpv_render_context_set_update_callback( + self->mpv_gl, mpv_render_update, self); + + self->tick_source = g_timeout_add(250, (GSourceFunc)mpv_widget_tick, self); +} + +static void +mpv_widget_on_unrealize(GtkWidget *widget, gpointer user_data) +{ + MpvWidget *self = MPV_WIDGET(user_data); + + if (self->tick_source) { + g_source_remove(self->tick_source); + self->tick_source = 0; + } + + if (self->mpv_gl) { + gtk_gl_area_make_current(GTK_GL_AREA(widget)); + mpv_render_context_free(self->mpv_gl); + self->mpv_gl = NULL; + } + + if (self->mpv) { + mpv_terminate_destroy(self->mpv); + self->mpv = NULL; + } +} + +static gboolean +mpv_widget_on_render(GtkGLArea *area, GdkGLContext *context, gpointer user_data) +{ + (void)context; + MpvWidget *self = MPV_WIDGET(user_data); + + if (!self->mpv_gl) + return TRUE; + + int width = gtk_widget_get_width(GTK_WIDGET(area)); + int height = gtk_widget_get_height(GTK_WIDGET(area)); + + if (width <= 0 || height <= 0) + return TRUE; + + mpv_opengl_fbo fbo = { + .fbo = 0, + .w = width, + .h = height, + }; + + int flip_y = 1; + + mpv_render_param params[] = { + {MPV_RENDER_PARAM_OPENGL_FBO, &fbo}, + {MPV_RENDER_PARAM_FLIP_Y, &flip_y}, + {0} + }; + + mpv_render_context_render(self->mpv_gl, params); + + return TRUE; +} + +static gboolean +mpv_widget_tick(gpointer user_data) +{ + MpvWidget *self = MPV_WIDGET(user_data); + + if (!self->mpv) + return G_SOURCE_CONTINUE; + + mpv_event *event = mpv_wait_event(self->mpv, 0); + (void)event; + + double pos = 0; + if (mpv_get_property(self->mpv, "time-pos", + MPV_FORMAT_DOUBLE, &pos) == MPV_ERROR_SUCCESS) + { + if (fabs(pos - self->position) > 0.1) { + self->position = pos; + g_signal_emit(self, signals[SIGNAL_POSITION_CHANGED], 0, pos); + } + } + + double dur = 0; + if (mpv_get_property(self->mpv, "duration", + MPV_FORMAT_DOUBLE, &dur) == MPV_ERROR_SUCCESS) + { + if (fabs(dur - self->duration) > 0.1) { + self->duration = dur; + g_signal_emit(self, signals[SIGNAL_DURATION_CHANGED], 0, dur); + } + } + + int paused = 0; + if (mpv_get_property(self->mpv, "pause", + MPV_FORMAT_FLAG, &paused) == MPV_ERROR_SUCCESS) + { + gboolean p = !!paused; + if (p != self->paused) { + self->paused = p; + g_signal_emit(self, signals[SIGNAL_PLAYBACK_STATE_CHANGED], 0, p); + } + } + + return G_SOURCE_CONTINUE; +} + +static void +mpv_widget_size_allocate(GtkWidget *widget, + int width, int height, int baseline) +{ + GtkWidget *child = gtk_widget_get_first_child(widget); + if (!child) + return; + + GtkAllocation alloc = { + .x = 0, + .y = 0, + .width = width, + .height = height, + }; + gtk_widget_size_allocate(child, &alloc, baseline); +} + +static void +mpv_widget_measure(GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + GtkWidget *child = gtk_widget_get_first_child(widget); + if (child && gtk_widget_get_visible(child)) { + gtk_widget_measure(child, orientation, for_size, + minimum, natural, + minimum_baseline, natural_baseline); + } else { + if (minimum) *minimum = 0; + if (natural) *natural = 0; + if (minimum_baseline) *minimum_baseline = -1; + if (natural_baseline) *natural_baseline = -1; + } +} + +static void +mpv_widget_dispose(GObject *object) +{ + MpvWidget *self = MPV_WIDGET(object); + + if (self->tick_source) { + g_source_remove(self->tick_source); + self->tick_source = 0; + } + + if (self->gl_area) { + gtk_widget_unparent(self->gl_area); + self->gl_area = NULL; + } + + G_OBJECT_CLASS(mpv_widget_parent_class)->dispose(object); +} + +static void +mpv_widget_init(MpvWidget *self) +{ + self->gl_area = gtk_gl_area_new(); + gtk_widget_set_hexpand(self->gl_area, TRUE); + gtk_widget_set_vexpand(self->gl_area, TRUE); + + g_signal_connect(self->gl_area, "realize", + G_CALLBACK(mpv_widget_on_realize), self); + g_signal_connect(self->gl_area, "unrealize", + G_CALLBACK(mpv_widget_on_unrealize), self); + g_signal_connect(self->gl_area, "render", + G_CALLBACK(mpv_widget_on_render), self); + + gtk_widget_set_parent(self->gl_area, GTK_WIDGET(self)); + + self->position = 0.0; + self->duration = 0.0; + self->paused = TRUE; + self->tick_source = 0; +} + +static void +mpv_widget_class_init(MpvWidgetClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + gobject_class->dispose = mpv_widget_dispose; + widget_class->size_allocate = mpv_widget_size_allocate; + widget_class->measure = mpv_widget_measure; + + signals[SIGNAL_POSITION_CHANGED] = g_signal_new( + "position-changed", + MPV_TYPE_WIDGET, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__DOUBLE, + G_TYPE_NONE, 1, G_TYPE_DOUBLE); + + signals[SIGNAL_DURATION_CHANGED] = g_signal_new( + "duration-changed", + MPV_TYPE_WIDGET, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__DOUBLE, + G_TYPE_NONE, 1, G_TYPE_DOUBLE); + + signals[SIGNAL_PLAYBACK_STATE_CHANGED] = g_signal_new( + "playback-state-changed", + MPV_TYPE_WIDGET, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); + + signals[SIGNAL_ERROR] = g_signal_new( + "error", + MPV_TYPE_WIDGET, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); + + gtk_widget_class_set_css_name(widget_class, "mpvwidget"); + gtk_widget_class_set_accessible_role(widget_class, + GTK_ACCESSIBLE_ROLE_GENERIC); +} + +GtkWidget * +mpv_widget_new(void) +{ + return g_object_new(MPV_TYPE_WIDGET, NULL); +} + +void +mpv_widget_load_url(MpvWidget *self, const char *url) +{ + g_return_if_fail(MPV_IS_WIDGET(self)); + g_return_if_fail(url != NULL); + + if (!self->mpv) + return; + + const char *cmd[] = {"loadfile", url, NULL}; + mpv_command(self->mpv, cmd); + mpv_set_property_string(self->mpv, "pause", "yes"); +} + +void +mpv_widget_play(MpvWidget *self) +{ + g_return_if_fail(MPV_IS_WIDGET(self)); + + if (self->mpv) + mpv_set_property_string(self->mpv, "pause", "no"); +} + +void +mpv_widget_pause(MpvWidget *self) +{ + g_return_if_fail(MPV_IS_WIDGET(self)); + + if (self->mpv) + mpv_set_property_string(self->mpv, "pause", "yes"); +} + +void +mpv_widget_seek_relative(MpvWidget *self, double offset_seconds) +{ + g_return_if_fail(MPV_IS_WIDGET(self)); + + if (!self->mpv) + return; + + char *offset_str = g_strdup_printf("%+.1f", offset_seconds); + const char *cmd[] = {"seek", offset_str, "relative", NULL}; + mpv_command(self->mpv, cmd); + g_free(offset_str); +} + +void +mpv_widget_seek_absolute(MpvWidget *self, double position_seconds) +{ + g_return_if_fail(MPV_IS_WIDGET(self)); + + if (!self->mpv) + return; + + char *pos_str = g_strdup_printf("%.1f", position_seconds); + const char *cmd[] = {"seek", pos_str, "absolute", NULL}; + mpv_command(self->mpv, cmd); + g_free(pos_str); +} + +void +mpv_widget_set_volume(MpvWidget *self, double volume) +{ + g_return_if_fail(MPV_IS_WIDGET(self)); + + if (!self->mpv) + return; + + char *vol_str = g_strdup_printf("%.0f", volume); + mpv_set_property_string(self->mpv, "volume", vol_str); + g_free(vol_str); +} + +double +mpv_widget_get_position(MpvWidget *self) +{ + g_return_val_if_fail(MPV_IS_WIDGET(self), 0.0); + return self->position; +} + +double +mpv_widget_get_duration(MpvWidget *self) +{ + g_return_val_if_fail(MPV_IS_WIDGET(self), 0.0); + return self->duration; +} + +gboolean +mpv_widget_get_paused(MpvWidget *self) +{ + g_return_val_if_fail(MPV_IS_WIDGET(self), FALSE); + return self->paused; +} + +double +mpv_widget_get_volume(MpvWidget *self) +{ + g_return_val_if_fail(MPV_IS_WIDGET(self), 0.0); + + if (!self->mpv) + return 100.0; + + double vol = 100.0; + mpv_get_property(self->mpv, "volume", MPV_FORMAT_DOUBLE, &vol); + return vol; +}