Files
delfin/video_player_mpv/sys/video-player-mpv/video-player-mpv.c

530 lines
18 KiB
C

#include "video-player-mpv.h"
#include "video-player-mpv/track.h"
#include <epoxy/egl.h>
#include <epoxy/glx.h>
#include <gtk/gtk.h>
#include <locale.h>
#include <mpv/client.h>
#include <mpv/render.h>
#include <mpv/render_gl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/wayland/gdkwayland.h>
#endif
#ifdef GDK_WINDOWING_X11
#include <gdk/x11/gdkx.h>
#endif
#include "video-player-mpv/track-list.h"
// TODO: MPV was calling mpv_update_callback while/after the player was
// destroyed. :(
static gboolean destroyed = false;
enum {
SIGNAL_POSITION_UPDATED,
SIGNAL_DURATION_UPDATED,
SIGNAL_TRACKS_UPDATED,
SIGNAL_VOLUME_UPDATED,
SIGNAL_MUTE_UPDATED,
SIGNAL_END_OF_FILE,
SIGNAL_SEEKING,
SIGNAL_CORE_IDLE,
SIGNAL_CURRENT_AO,
SIGNAL_PAUSE,
SIGNAL_LAST,
};
static guint signals[SIGNAL_LAST] = {
0,
};
struct MpvCtx {
mpv_handle *handle;
mpv_render_context *render_context;
int width;
int height;
guint event_source_id;
};
struct _VpmVideoPlayerMpv {
GtkGLArea parent;
struct MpvCtx *mpv_ctx;
};
G_DEFINE_TYPE(VpmVideoPlayerMpv, vpm_video_player_mpv, GTK_TYPE_GL_AREA);
static void vpm_video_player_mpv_class_init(VpmVideoPlayerMpvClass *klass) {
gtk_widget_class_set_css_name(GTK_WIDGET_CLASS(klass), "VideoPlayerMpv");
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_string(provider, "VideoPlayerMpv {"
" background: black;"
"}");
gtk_style_context_add_provider_for_display(
gdk_display_get_default(), GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
signals[SIGNAL_DURATION_UPDATED] = g_signal_new(
"duration-updated", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL,
NULL, NULL, G_TYPE_NONE, 1, G_TYPE_DOUBLE);
signals[SIGNAL_POSITION_UPDATED] = g_signal_new(
"position-updated", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL,
NULL, NULL, G_TYPE_NONE, 1, G_TYPE_DOUBLE);
signals[SIGNAL_TRACKS_UPDATED] = g_signal_new(
"tracks-updated", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL,
NULL, NULL, G_TYPE_NONE, 1, VPM_TYPE_TRACK_LIST);
signals[SIGNAL_VOLUME_UPDATED] = g_signal_new(
"volume-updated", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, NULL,
NULL, NULL, G_TYPE_NONE, 1, G_TYPE_DOUBLE);
signals[SIGNAL_MUTE_UPDATED] =
g_signal_new("mute-updated", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
signals[SIGNAL_END_OF_FILE] =
g_signal_new("end-of-file", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST,
0, NULL, NULL, NULL, G_TYPE_NONE, 0);
signals[SIGNAL_SEEKING] =
g_signal_new("seeking", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0,
NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
signals[SIGNAL_CORE_IDLE] =
g_signal_new("core-idle", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0,
NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
signals[SIGNAL_CURRENT_AO] =
g_signal_new("current-ao", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0,
NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
signals[SIGNAL_PAUSE] =
g_signal_new("pause", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0,
NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
}
static void *get_proc_address(void *fn_ctx, const gchar *name) {
GdkDisplay *display = gdk_display_get_default();
#ifdef GDK_WINDOWING_WAYLAND
if (GDK_IS_WAYLAND_DISPLAY(display)) {
return eglGetProcAddress(name);
}
#endif
#ifdef GDK_WINDOWING_X11
if (GDK_IS_X11_DISPLAY(display)) {
return (void *)(intptr_t)glXGetProcAddressARB((const GLubyte *)name);
}
#endif
#ifdef GDK_WINDOWING_WIN32
if (GDK_IS_WIN32_DISPLAY(display)) {
return wglGetProcAddress(name);
}
#endif
g_assert_not_reached();
return NULL;
}
gboolean process_events(gpointer data) {
if (destroyed) {
return FALSE;
}
struct _VpmVideoPlayerMpv *widget = (struct _VpmVideoPlayerMpv *)data;
int done = 0;
gtk_gl_area_queue_render(GTK_GL_AREA(&widget->parent));
while (!done) {
mpv_event *event = mpv_wait_event(widget->mpv_ctx->handle, 0);
switch (event->event_id) {
case MPV_EVENT_NONE:
done = 1;
break;
case MPV_EVENT_LOG_MESSAGE:
// printf("mpv log: %s\n", ((mpv_event_log_message *)event->data)->text);
break;
case MPV_EVENT_END_FILE:
// TODO: might want to include reason (eof, error, redirect, etc.)
g_signal_emit(widget, signals[SIGNAL_END_OF_FILE], 0);
break;
case MPV_EVENT_GET_PROPERTY_REPLY: {
// TODO: We should probably check the error value here
if (event->error < 0) {
fprintf(stderr, "Error getting property reply\n");
break;
}
__attribute__((fallthrough));
}
case MPV_EVENT_PROPERTY_CHANGE: {
mpv_event_property *prop = (mpv_event_property *)event->data;
switch (prop->format) {
case MPV_FORMAT_DOUBLE:
if (strcmp(prop->name, "duration") == 0) {
g_signal_emit(widget, signals[SIGNAL_DURATION_UPDATED], 0,
*(double *)prop->data);
} else if (strcmp(prop->name, "time-pos") == 0) {
g_signal_emit(widget, signals[SIGNAL_POSITION_UPDATED], 0,
*(double *)prop->data);
} else if (strcmp(prop->name, "ao-volume") == 0) {
g_signal_emit(widget, signals[SIGNAL_VOLUME_UPDATED], 0,
*(double *)prop->data);
}
break;
case MPV_FORMAT_NODE:
if (strcmp(prop->name, "track-list") == 0) {
mpv_node *data = prop->data;
if (data->format == MPV_FORMAT_NODE_ARRAY) {
GList *tracks = NULL;
for (int i = 0; i < data->u.list->num; i++) {
mpv_node trackNode = data->u.list->values[i];
VpmTrack *track = track_node_to_track(trackNode);
tracks = g_list_prepend(tracks, track);
}
VpmTrackList track_list = {.tracks = tracks};
g_signal_emit(widget, signals[SIGNAL_TRACKS_UPDATED], 0,
&track_list);
}
}
break;
case MPV_FORMAT_FLAG: {
gboolean val = *(int *)prop->data == 1;
if (strcmp(prop->name, "ao-mute") == 0) {
g_signal_emit(widget, signals[SIGNAL_MUTE_UPDATED], 0, val);
} else if (strcmp(prop->name, "seeking") == 0) {
g_signal_emit(widget, signals[SIGNAL_SEEKING], 0, val);
// Ensure pause state stays correct after seeking
mpv_get_property_async(widget->mpv_ctx->handle, 0, "pause",
MPV_FORMAT_FLAG);
} else if (strcmp(prop->name, "core-idle") == 0) {
g_signal_emit(widget, signals[SIGNAL_CORE_IDLE], 0, val);
} else if (strcmp(prop->name, "pause") == 0) {
g_signal_emit(widget, signals[SIGNAL_PAUSE], 0, val);
}
}
break;
case MPV_FORMAT_STRING:
if (strcmp(prop->name, "current-ao") == 0) {
char *val = *(char **)prop->data;
g_signal_emit(widget, signals[SIGNAL_CURRENT_AO], 0, val);
// Once the audio output has been initialized, we want to emit update
// signals for mute/volume to ensure we got the correct value
mpv_get_property_async(widget->mpv_ctx->handle, 0, "ao-mute",
MPV_FORMAT_FLAG);
mpv_get_property_async(widget->mpv_ctx->handle, 0, "ao-volume",
MPV_FORMAT_DOUBLE);
}
break;
default:
break;
}
} break;
default:;
}
}
return FALSE;
}
static void mpv_update_callback(void *ctx) {
struct _VpmVideoPlayerMpv *widget = (struct _VpmVideoPlayerMpv *)ctx;
if (destroyed) {
return;
}
widget->mpv_ctx->event_source_id =
g_idle_add_full(G_PRIORITY_HIGH_IDLE, process_events, ctx, NULL);
}
static void realize(GtkGLArea *area) { gtk_gl_area_make_current(area); }
static void resize(GtkGLArea *area, int width, int height, gpointer data) {
struct MpvCtx *mpv_ctx = (struct MpvCtx *)data;
mpv_ctx->width = width;
mpv_ctx->height = height;
}
static void destroy(GtkGLArea *area, gpointer data) {
destroyed = true;
struct MpvCtx *mpv_ctx = (struct MpvCtx *)data;
mpv_render_context_set_update_callback(mpv_ctx->render_context, NULL, NULL);
mpv_render_context_free(mpv_ctx->render_context);
mpv_terminate_destroy(mpv_ctx->handle);
free(mpv_ctx);
mpv_ctx = NULL;
}
gboolean render(GtkGLArea *widget, GdkGLContext *ctx, gpointer user_data);
static void vpm_video_player_mpv_init(VpmVideoPlayerMpv *widget) {
setlocale(LC_NUMERIC, "C");
struct MpvCtx *mpv_ctx = malloc(sizeof *mpv_ctx);
mpv_ctx->handle = mpv_create();
mpv_initialize(mpv_ctx->handle);
mpv_request_log_messages(mpv_ctx->handle, "debug");
widget->mpv_ctx = mpv_ctx;
destroyed = FALSE;
mpv_render_param params[] = {
{MPV_RENDER_PARAM_API_TYPE, MPV_RENDER_API_TYPE_OPENGL},
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS,
&(mpv_opengl_init_params){
.get_proc_address = get_proc_address,
}},
{MPV_RENDER_PARAM_ADVANCED_CONTROL, &(int){0}},
{0}};
mpv_render_context_create(&mpv_ctx->render_context, mpv_ctx->handle, params);
mpv_set_wakeup_callback(mpv_ctx->handle, mpv_update_callback, widget);
mpv_render_context_set_update_callback(mpv_ctx->render_context,
mpv_update_callback, widget);
mpv_observe_property(mpv_ctx->handle, 0, "time-pos", MPV_FORMAT_DOUBLE);
mpv_observe_property(mpv_ctx->handle, 0, "duration", MPV_FORMAT_DOUBLE);
mpv_observe_property(mpv_ctx->handle, 0, "track-list", MPV_FORMAT_NODE);
mpv_observe_property(mpv_ctx->handle, 0, "ao-volume", MPV_FORMAT_DOUBLE);
mpv_observe_property(mpv_ctx->handle, 0, "ao-mute", MPV_FORMAT_FLAG);
mpv_observe_property(mpv_ctx->handle, 0, "mute", MPV_FORMAT_FLAG);
mpv_observe_property(mpv_ctx->handle, 0, "seeking", MPV_FORMAT_FLAG);
mpv_observe_property(mpv_ctx->handle, 0, "core-idle", MPV_FORMAT_FLAG);
mpv_observe_property(mpv_ctx->handle, 0, "current-ao", MPV_FORMAT_STRING);
mpv_observe_property(mpv_ctx->handle, 0, "pause", MPV_FORMAT_FLAG);
g_signal_connect(&widget->parent, "realize", G_CALLBACK(realize), NULL);
g_signal_connect(&widget->parent, "render", G_CALLBACK(render), mpv_ctx);
g_signal_connect(&widget->parent, "resize", G_CALLBACK(resize), mpv_ctx);
g_signal_connect(&widget->parent, "destroy", G_CALLBACK(destroy), mpv_ctx);
}
GtkWidget *vpm_video_player_mpv_new() {
return g_object_new(vpm_video_player_mpv_get_type(), NULL);
}
gboolean render(GtkGLArea *widget, GdkGLContext *ctx, gpointer data) {
struct MpvCtx *mpv_ctx = (struct MpvCtx *)data;
if ((mpv_render_context_update(mpv_ctx->render_context) &
MPV_RENDER_UPDATE_FRAME)) {
gint fbo = -1;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
mpv_opengl_fbo opengl_fbo = {fbo, mpv_ctx->width, mpv_ctx->height, 0};
mpv_render_param params[] = {{MPV_RENDER_PARAM_OPENGL_FBO, &opengl_fbo},
{MPV_RENDER_PARAM_FLIP_Y, &(int){1}},
{MPV_RENDER_PARAM_INVALID, NULL}};
mpv_render_context_render(mpv_ctx->render_context, params);
}
gtk_gl_area_queue_render(widget);
return TRUE;
}
void vpm_video_player_mpv_play_uri(VpmVideoPlayerMpv *self, const char *uri) {
const char *cmd[] = {
"loadfile",
uri,
NULL,
};
mpv_command(self->mpv_ctx->handle, cmd);
}
void vpm_video_player_mpv_play(VpmVideoPlayerMpv *self) {
int val = FALSE;
mpv_set_property(self->mpv_ctx->handle, "pause", MPV_FORMAT_FLAG, &val);
}
void vpm_video_player_mpv_pause(VpmVideoPlayerMpv *self) {
int val = TRUE;
mpv_set_property(self->mpv_ctx->handle, "pause", MPV_FORMAT_FLAG, &val);
}
void vpm_video_player_mpv_stop(VpmVideoPlayerMpv *self) {
const char *cmd[] = {
"stop",
NULL,
};
mpv_command(self->mpv_ctx->handle, cmd);
}
double vpm_video_player_mpv_position(VpmVideoPlayerMpv *self) {
double res;
if (mpv_get_property(self->mpv_ctx->handle, "time-pos", MPV_FORMAT_DOUBLE,
&res) < 0) {
fprintf(stderr, "Error getting time-pos.\n");
return 0;
}
return res;
}
void vpm_video_player_mpv_seek_to(VpmVideoPlayerMpv *self, uint seconds) {
gchar *val = g_strdup_printf("%i", seconds);
const char *cmd[] = {"seek", val, "absolute", NULL};
mpv_command(self->mpv_ctx->handle, cmd);
}
void vpm_video_player_mpv_seek_by(VpmVideoPlayerMpv *self, int seconds) {
gchar *val = g_strdup_printf("%i", seconds);
const char *cmd[] = {"seek", val, "relative", NULL};
mpv_command(self->mpv_ctx->handle, cmd);
}
void vpm_video_player_mpv_frame_step_forwards(VpmVideoPlayerMpv *self) {
const char *cmd[] = {"frame-step", NULL};
mpv_command(self->mpv_ctx->handle, cmd);
}
void vpm_video_player_mpv_frame_step_backwards(VpmVideoPlayerMpv *self) {
const char *cmd[] = {"frame-back-step", NULL};
mpv_command(self->mpv_ctx->handle, cmd);
}
bool vpm_video_player_mpv_mute(VpmVideoPlayerMpv *self) {
bool mute = false;
if (mpv_get_property(self->mpv_ctx->handle, "ao-mute", MPV_FORMAT_FLAG,
&mute) < 0) {
fprintf(stderr, "Error getting ao-mute.\n");
return mute;
}
return mute;
}
void vpm_video_player_mpv_set_mute(VpmVideoPlayerMpv *self, bool mute) {
int val = mute;
mpv_set_property(self->mpv_ctx->handle, "ao-mute", MPV_FORMAT_FLAG, &val);
}
double vpm_video_player_mpv_volume(VpmVideoPlayerMpv *self) {
double volume = 1;
if (mpv_set_property(self->mpv_ctx->handle, "ao-volume", MPV_FORMAT_DOUBLE,
&volume) < 0) {
fprintf(stderr, "Error getting ao-volume.\n");
}
return volume;
}
void vpm_video_player_mpv_set_volume(VpmVideoPlayerMpv *self, double volume) {
mpv_set_property(self->mpv_ctx->handle, "ao-volume", MPV_FORMAT_DOUBLE,
&volume);
}
int vpm_video_player_mpv_current_audio_track(VpmVideoPlayerMpv *self) {
static int id;
if (mpv_get_property(self->mpv_ctx->handle, "current-tracks/audio/id",
MPV_FORMAT_INT64, &id) < 0) {
fprintf(stderr, "Error getting current-tracks/audio/id.\n");
return -1;
}
return id;
}
void vpm_video_player_mpv_set_audio_track(VpmVideoPlayerMpv *self,
uint audio_track_id) {
uint64_t id = (uint64_t)audio_track_id;
mpv_set_property(self->mpv_ctx->handle, "aid", MPV_FORMAT_INT64, &id);
}
int vpm_video_player_mpv_current_subtitle_track(VpmVideoPlayerMpv *self) {
static int id;
if (mpv_get_property(self->mpv_ctx->handle, "current-tracks/sub/id",
MPV_FORMAT_INT64, &id) < 0) {
fprintf(stderr, "Error getting current-tracks/sub/id.\n");
return -1;
}
return id;
}
void vpm_video_player_mpv_set_subtitle_track(VpmVideoPlayerMpv *self,
uint subtitle_track_id) {
uint64_t id = (uint64_t)subtitle_track_id;
mpv_set_property(self->mpv_ctx->handle, "sid", MPV_FORMAT_INT64, &id);
}
void vpm_video_player_mpv_add_subtitle_track(VpmVideoPlayerMpv *self,
const char *url,
const char *title) {
const char *cmd[] = {
"sub-add", url, "auto", title, NULL,
};
if (mpv_command(self->mpv_ctx->handle, cmd) < 0) {
printf("Error adding subtitle track %s", title);
}
}
void vpm_video_player_mpv_set_subtitle_scale(VpmVideoPlayerMpv *self,
double subtitle_scale) {
mpv_set_option(self->mpv_ctx->handle, "sub-scale", MPV_FORMAT_DOUBLE,
&subtitle_scale);
}
void vpm_video_player_mpv_set_subtitle_colour(VpmVideoPlayerMpv *self,
char *colour) {
mpv_set_option(self->mpv_ctx->handle, "sub-color", MPV_FORMAT_STRING,
&colour);
}
void vpm_video_player_mpv_set_subtitle_background_colour(
VpmVideoPlayerMpv *self, char *colour) {
mpv_set_option(self->mpv_ctx->handle, "sub-back-color", MPV_FORMAT_STRING,
&colour);
}
void vpm_video_player_mpv_set_subtitle_position(VpmVideoPlayerMpv *self,
uint position) {
uint64_t position_int = (uint64_t)position;
mpv_set_option(self->mpv_ctx->handle, "sub-pos", MPV_FORMAT_INT64,
&position_int);
}
void vpm_video_player_mpv_set_subtitle_font_family(VpmVideoPlayerMpv *self,
char *family) {
int err = mpv_set_option(self->mpv_ctx->handle, "sub-font", MPV_FORMAT_STRING,
&family);
if (err < 0) {
printf("Error setting sub-font: %d\n", err);
}
}
void vpm_video_player_mpv_set_subtitle_font_size(VpmVideoPlayerMpv *self,
uint size) {
uint64_t size_int = (uint64_t)size;
int err = mpv_set_option(self->mpv_ctx->handle, "sub-font-size",
MPV_FORMAT_INT64, &size_int);
if (err < 0) {
printf("Error setting sub-font-size: %d\n", err);
}
}
void vpm_video_player_mpv_set_subtitle_font_bold(VpmVideoPlayerMpv *self,
bool bold) {
int val = bold;
mpv_set_property(self->mpv_ctx->handle, "sub-bold", MPV_FORMAT_FLAG, &val);
}
void vpm_video_player_mpv_set_subtitle_font_italic(VpmVideoPlayerMpv *self,
bool italic) {
int val = italic;
mpv_set_property(self->mpv_ctx->handle, "sub-italic", MPV_FORMAT_FLAG, &val);
}