Add mpv_widget C GObject with GtkGLArea rendering

This commit is contained in:
Brendan Szymanski 2026-06-24 00:08:46 -04:00
parent 5bf43cf375
commit 580cd1fe2b
2 changed files with 465 additions and 0 deletions

View file

@ -0,0 +1,36 @@
#ifndef MPV_WIDGET_H
#define MPV_WIDGET_H
#include <gtk/gtk.h>
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 */

View file

@ -0,0 +1,429 @@
#include "mpv_widget.h"
#include <mpv/client.h>
#include <mpv/render_gl.h>
#include <dlfcn.h>
#include <math.h>
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;
}