Add mpv_widget C GObject with GtkGLArea rendering
This commit is contained in:
parent
5bf43cf375
commit
580cd1fe2b
2 changed files with 465 additions and 0 deletions
36
Sources/CModules/include/mpv_widget.h
Normal file
36
Sources/CModules/include/mpv_widget.h
Normal 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 */
|
||||
429
Sources/CModules/mpv_widget.c
Normal file
429
Sources/CModules/mpv_widget.c
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue