439 lines
11 KiB
C
439 lines
11 KiB
C
#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, "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;
|
|
}
|