#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, "hwdec", "auto"); mpv_set_option_string(self->mpv, "keep-open", "yes"); 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; while (self->mpv) { mpv_event *event = mpv_wait_event(self->mpv, 0); if (event->event_id == MPV_EVENT_NONE) break; if (event->event_id == MPV_EVENT_ERROR) { const char *msg = "mpv playback error"; if (event->error && event->error->string) msg = event->error->string; g_signal_emit(self, signals[SIGNAL_ERROR], 0, msg); } if (event->event_id == MPV_EVENT_END_FILE) { g_signal_emit(self, signals[SIGNAL_PLAYBACK_STATE_CHANGED], 0, TRUE); } } 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; }