#include "player.h" #include #include #include #include #include #include struct Player { mpv_handle *mpv; unsigned long wid; PlayerEventCallback event_callback; gpointer event_callback_data; gboolean initialized; char *config_path; }; static void check_error(int status) { if (status < 0) { fprintf(stderr, "mpv API error: %s\n", mpv_error_string(status)); } } Player* player_new(void) { if (load_mpv_library() < 0) { return NULL; } Player *player = g_new0(Player, 1); player->mpv = mpv_create(); if (!player->mpv) { fprintf(stderr, "Failed to create mpv context\n"); g_free(player); return NULL; } player->wid = 0; player->event_callback = NULL; player->event_callback_data = NULL; player->initialized = FALSE; return player; } void player_destroy(Player *player) { if (!player) return; if (player->mpv) { mpv_terminate_destroy(player->mpv); } g_free(player->config_path); g_free(player); } void player_set_window(Player *player, unsigned long wid) { if (!player) return; player->wid = wid; } void player_set_config_path(Player *player, const char *path) { if (!player) return; g_free(player->config_path); player->config_path = path ? g_strdup(path) : NULL; } static void wakeup_callback(void *ctx) { /* This is called from mpv's thread. We need to wake up the GTK main loop. */ g_idle_add_full(G_PRIORITY_HIGH, (GSourceFunc)player_process_events, ctx, NULL); } static void load_scripts(mpv_handle *mpv) { const char *config_dir = g_get_user_config_dir(); char *scripts_dir = g_build_filename(config_dir, "gtk2-media-player", "scripts", NULL); printf("Scanning for scripts in: %s\n", scripts_dir); /* Create scripts directory if it doesn't exist */ g_mkdir_with_parents(scripts_dir, 0755); DIR *dir = opendir(scripts_dir); if (!dir) { printf("Failed to open scripts directory!\n"); } else { struct dirent *entry; int count = 0; while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') continue; char *path = g_build_filename(scripts_dir, entry->d_name, NULL); size_t len = strlen(entry->d_name); /* Check extension (Lua or JS) */ if ((len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0) || (len > 3 && strcmp(entry->d_name + len - 3, ".js") == 0)) { printf("Loading script: %s\n", path); /* Use load-script command (works after mpv_initialize) */ const char *cmd[] = {"load-script", path, NULL}; int res = mpv_command(mpv, cmd); if (res < 0) { printf(" -> Failed! Error: %s\n", mpv_error_string(res)); } else { printf(" -> OK\n"); count++; } } g_free(path); } closedir(dir); printf("Loaded %d script(s)\n", count); } g_free(scripts_dir); } int player_init(Player *player) { if (!player || !player->mpv) return -1; /* Set window ID if provided - Must be before initialize */ if (player->wid != 0) { char wid_str[64]; snprintf(wid_str, sizeof(wid_str), "%lu", player->wid); check_error(mpv_set_option_string(player->mpv, "wid", wid_str)); } /* Basic configuration - Set BEFORE config loading so config can override defaults */ check_error(mpv_set_option_string(player->mpv, "input-default-bindings", "no")); check_error(mpv_set_option_string(player->mpv, "input-vo-keyboard", "no")); check_error(mpv_set_option_string(player->mpv, "osc", "no")); /* We have our own controls */ check_error(mpv_set_option_string(player->mpv, "osd-bar", "yes")); check_error(mpv_set_option_string(player->mpv, "osd-duration", "2000")); check_error(mpv_set_option_string(player->mpv, "input-cursor", "no")); /* Let GTK handle the cursor */ check_error(mpv_set_option_string(player->mpv, "keep-open", "yes")); check_error(mpv_set_option_string(player->mpv, "idle", "yes")); /* Load external config file if provided */ if (player->config_path && g_file_test(player->config_path, G_FILE_TEST_EXISTS)) { check_error(mpv_set_option_string(player->mpv, "config", "yes")); check_error(mpv_set_option_string(player->mpv, "config-file", player->config_path)); } /* Initialize MPV */ int res = mpv_initialize(player->mpv); if (res < 0) { check_error(res); return -1; } /* Load scripts AFTER initialization using load-script command */ load_scripts(player->mpv); /* Set wakeup callback for event processing */ mpv_set_wakeup_callback(player->mpv, wakeup_callback, player); player->initialized = TRUE; return 0; } /* Playback commands */ void player_load_file(Player *player, const char *path, gboolean append) { if (!player || !player->mpv || !path) return; const char *cmd[] = {"loadfile", path, append ? "append-play" : "replace", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_play(Player *player) { if (!player || !player->mpv) return; int flag = 0; mpv_set_property_async(player->mpv, 0, "pause", MPV_FORMAT_FLAG, &flag); } void player_pause(Player *player) { if (!player || !player->mpv) return; int flag = 1; mpv_set_property_async(player->mpv, 0, "pause", MPV_FORMAT_FLAG, &flag); } void player_toggle_pause(Player *player) { if (!player || !player->mpv) return; const char *cmd[] = {"cycle", "pause", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_stop(Player *player) { if (!player || !player->mpv) return; const char *cmd[] = {"stop", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_seek(Player *player, double seconds) { if (!player || !player->mpv) return; char sec_str[32]; snprintf(sec_str, sizeof(sec_str), "%f", seconds); const char *cmd[] = {"seek", sec_str, "relative", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); /* Show progress OSD */ const char *osd_cmd[] = {"show-progress", NULL}; mpv_command_async(player->mpv, 0, osd_cmd); } void player_seek_absolute(Player *player, double position) { if (!player || !player->mpv) return; char pos_str[32]; snprintf(pos_str, sizeof(pos_str), "%f", position); const char *cmd[] = {"seek", pos_str, "absolute+exact", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); /* Show progress OSD */ const char *osd_cmd[] = {"show-progress", NULL}; mpv_command_async(player->mpv, 0, osd_cmd); } void player_frame_step(Player *player) { if (!player || !player->mpv) return; const char *cmd[] = {"frame-step", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_frame_back_step(Player *player) { if (!player || !player->mpv) return; const char *cmd[] = {"frame-back-step", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_set_option_string(Player *player, const char *name, const char *value) { if (!player || !player->mpv) return; if (player->initialized) { check_error(mpv_set_property_string(player->mpv, name, value)); } else { check_error(mpv_set_option_string(player->mpv, name, value)); } } void player_set_option_flag(Player *player, const char *name, int value) { if (!player || !player->mpv) return; if (player->initialized) { const char *val = value ? "yes" : "no"; check_error(mpv_set_property_string(player->mpv, name, val)); } else { check_error(mpv_set_option_string(player->mpv, name, value ? "yes" : "no")); } } /* Property getters */ double player_get_duration(Player *player) { if (!player || !player->mpv) return 0.0; double duration = 0.0; mpv_get_property(player->mpv, "duration", MPV_FORMAT_DOUBLE, &duration); return duration; } double player_get_position(Player *player) { if (!player || !player->mpv) return 0.0; double position = 0.0; mpv_get_property(player->mpv, "time-pos", MPV_FORMAT_DOUBLE, &position); return position; } double player_get_volume(Player *player) { if (!player || !player->mpv) return 100.0; double volume = 100.0; mpv_get_property(player->mpv, "volume", MPV_FORMAT_DOUBLE, &volume); return volume; } gboolean player_get_paused(Player *player) { if (!player || !player->mpv) return TRUE; int paused = 1; mpv_get_property(player->mpv, "pause", MPV_FORMAT_FLAG, &paused); return paused ? TRUE : FALSE; } gboolean player_get_muted(Player *player) { if (!player || !player->mpv) return FALSE; int muted = 0; mpv_get_property(player->mpv, "mute", MPV_FORMAT_FLAG, &muted); return muted ? TRUE : FALSE; } double player_get_speed(Player *player) { if (!player || !player->mpv) return 1.0; double speed = 1.0; mpv_get_property(player->mpv, "speed", MPV_FORMAT_DOUBLE, &speed); return speed; } char* player_get_media_title(Player *player) { if (!player || !player->mpv) return NULL; char *title = NULL; mpv_get_property(player->mpv, "media-title", MPV_FORMAT_STRING, &title); if (title) { char *result = g_strdup(title); mpv_free(title); return result; } return NULL; } char* player_get_filename(Player *player) { if (!player || !player->mpv) return NULL; char *filename = NULL; mpv_get_property(player->mpv, "filename", MPV_FORMAT_STRING, &filename); if (filename) { char *result = g_strdup(filename); mpv_free(filename); return result; } return NULL; } gboolean player_get_eof_reached(Player *player) { if (!player || !player->mpv) return FALSE; int flag = 0; int status = mpv_get_property(player->mpv, "eof-reached", MPV_FORMAT_FLAG, &flag); if (status < 0) { return FALSE; } return flag ? TRUE : FALSE; } gboolean player_is_idle(Player *player) { if (!player || !player->mpv) return TRUE; int idle = 1; if (mpv_get_property(player->mpv, "idle-active", MPV_FORMAT_FLAG, &idle) < 0) { return TRUE; } /* Double check: if it says not idle, ensure we have a file loaded */ if (!idle) { char *path = NULL; if (mpv_get_property(player->mpv, "path", MPV_FORMAT_STRING, &path) < 0 || !path) { return TRUE; } if (path) mpv_free(path); } return idle ? TRUE : FALSE; } char* player_get_metadata_tag(Player *player, const char *tag) { if (!player || !player->mpv || !tag) return NULL; char prop[128]; snprintf(prop, sizeof(prop), "metadata/by-key/%s", tag); char *value = NULL; mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &value); if (value) { char *result = g_strdup(value); mpv_free(value); return result; } return NULL; } gboolean player_get_shuffle(Player *player) { if (!player || !player->mpv) return FALSE; int flag = 0; mpv_get_property(player->mpv, "shuffle", MPV_FORMAT_FLAG, &flag); return flag ? TRUE : FALSE; } gboolean player_get_loop(Player *player) { if (!player || !player->mpv) return FALSE; char *value = NULL; mpv_get_property(player->mpv, "loop-playlist", MPV_FORMAT_STRING, &value); gboolean result = FALSE; if (value) { if (strcmp(value, "no") != 0) result = TRUE; mpv_free(value); } return result; } /* Property setters */ void player_set_volume(Player *player, double volume) { if (!player || !player->mpv) return; char vol_str[32]; snprintf(vol_str, sizeof(vol_str), "%f", volume); if (player->initialized) { check_error(mpv_set_property_string(player->mpv, "volume", vol_str)); /* Show volume bar on OSD */ const char *cmd[] = {"osd-msg-bar", "set", "volume", NULL}; mpv_command_async(player->mpv, 0, cmd); } else { check_error(mpv_set_option_string(player->mpv, "volume", vol_str)); } } void player_set_muted(Player *player, gboolean muted) { if (!player || !player->mpv) return; if (player->initialized) { const char *val = muted ? "yes" : "no"; check_error(mpv_set_property_string(player->mpv, "mute", val)); } else { check_error(mpv_set_option_string(player->mpv, "mute", muted ? "yes" : "no")); } } void player_set_speed(Player *player, double speed) { if (!player || !player->mpv) return; char speed_str[32]; snprintf(speed_str, sizeof(speed_str), "%f", speed); check_error(mpv_set_property_string(player->mpv, "speed", speed_str)); } /* Track management */ int player_get_audio_track_count(Player *player) { if (!player || !player->mpv) return 0; int64_t count = 0; mpv_get_property(player->mpv, "track-list/count", MPV_FORMAT_INT64, &count); /* Count only audio tracks */ int audio_count = 0; for (int i = 0; i < count; i++) { char prop[64]; snprintf(prop, sizeof(prop), "track-list/%d/type", i); char *type = NULL; mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &type); if (type && strcmp(type, "audio") == 0) { audio_count++; } if (type) mpv_free(type); } return audio_count; } int player_get_subtitle_track_count(Player *player) { if (!player || !player->mpv) return 0; int64_t count = 0; mpv_get_property(player->mpv, "track-list/count", MPV_FORMAT_INT64, &count); /* Count only subtitle tracks */ int sub_count = 0; for (int i = 0; i < count; i++) { char prop[64]; snprintf(prop, sizeof(prop), "track-list/%d/type", i); char *type = NULL; mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &type); if (type && strcmp(type, "sub") == 0) { sub_count++; } if (type) mpv_free(type); } return sub_count; } int player_get_current_audio_track(Player *player) { if (!player || !player->mpv) return 0; int64_t aid = 0; mpv_get_property(player->mpv, "aid", MPV_FORMAT_INT64, &aid); return (int)aid; } int player_get_current_subtitle_track(Player *player) { if (!player || !player->mpv) return 0; int64_t sid = 0; mpv_get_property(player->mpv, "sid", MPV_FORMAT_INT64, &sid); return (int)sid; } void player_set_audio_track(Player *player, int track) { if (!player || !player->mpv) return; char aid_str[16]; snprintf(aid_str, sizeof(aid_str), "%d", track); check_error(mpv_set_property_string(player->mpv, "aid", aid_str)); } void player_set_subtitle_track(Player *player, int track) { if (!player || !player->mpv) return; char sid_str[16]; snprintf(sid_str, sizeof(sid_str), "%d", track); check_error(mpv_set_property_string(player->mpv, "sid", sid_str)); } void player_load_subtitle(Player *player, const char *path) { if (!player || !player->mpv || !path) return; const char *cmd[] = {"sub-add", path, NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } /* Track list helpers */ char** player_get_audio_track_list(Player *player, int *count) { if (!player || !player->mpv || !count) return NULL; int64_t total = 0; mpv_get_property(player->mpv, "track-list/count", MPV_FORMAT_INT64, &total); /* First pass: count audio tracks */ int audio_count = 0; for (int i = 0; i < total; i++) { char prop[64]; snprintf(prop, sizeof(prop), "track-list/%d/type", i); char *type = NULL; mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &type); if (type && strcmp(type, "audio") == 0) { audio_count++; } if (type) mpv_free(type); } *count = audio_count; if (audio_count == 0) return NULL; char **list = g_new0(char*, audio_count); int idx = 0; for (int i = 0; i < total && idx < audio_count; i++) { char prop[64]; snprintf(prop, sizeof(prop), "track-list/%d/type", i); char *type = NULL; mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &type); if (type && strcmp(type, "audio") == 0) { /* Get track title or lang */ char title_prop[64], lang_prop[64], id_prop[64]; snprintf(title_prop, sizeof(title_prop), "track-list/%d/title", i); snprintf(lang_prop, sizeof(lang_prop), "track-list/%d/lang", i); snprintf(id_prop, sizeof(id_prop), "track-list/%d/id", i); char *title = NULL, *lang = NULL; int64_t id = 0; mpv_get_property(player->mpv, title_prop, MPV_FORMAT_STRING, &title); mpv_get_property(player->mpv, lang_prop, MPV_FORMAT_STRING, &lang); mpv_get_property(player->mpv, id_prop, MPV_FORMAT_INT64, &id); if (title) { list[idx] = g_strdup_printf("Track %ld: %s", (long)id, title); } else if (lang) { list[idx] = g_strdup_printf("Track %ld: %s", (long)id, lang); } else { list[idx] = g_strdup_printf("Track %ld", (long)id); } if (title) mpv_free(title); if (lang) mpv_free(lang); idx++; } if (type) mpv_free(type); } return list; } void player_get_video_resolution(Player *player, int *w, int *h) { if (!player || !player->mpv) { if (w) *w = 0; if (h) *h = 0; return; } int64_t width = 0, height = 0; mpv_get_property(player->mpv, "dwidth", MPV_FORMAT_INT64, &width); mpv_get_property(player->mpv, "dheight", MPV_FORMAT_INT64, &height); if (w) *w = (int)width; if (h) *h = (int)height; } char** player_get_subtitle_track_list(Player *player, int *count) { if (!player || !player->mpv || !count) return NULL; int64_t total = 0; mpv_get_property(player->mpv, "track-list/count", MPV_FORMAT_INT64, &total); /* First pass: count subtitle tracks */ int sub_count = 0; for (int i = 0; i < total; i++) { char prop[64]; snprintf(prop, sizeof(prop), "track-list/%d/type", i); char *type = NULL; mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &type); if (type && strcmp(type, "sub") == 0) { sub_count++; } if (type) mpv_free(type); } *count = sub_count; if (sub_count == 0) return NULL; char **list = g_new0(char*, sub_count); int idx = 0; for (int i = 0; i < total && idx < sub_count; i++) { char prop[64]; snprintf(prop, sizeof(prop), "track-list/%d/type", i); char *type = NULL; mpv_get_property(player->mpv, prop, MPV_FORMAT_STRING, &type); if (type && strcmp(type, "sub") == 0) { char title_prop[64], lang_prop[64], id_prop[64]; snprintf(title_prop, sizeof(title_prop), "track-list/%d/title", i); snprintf(lang_prop, sizeof(lang_prop), "track-list/%d/lang", i); snprintf(id_prop, sizeof(id_prop), "track-list/%d/id", i); char *title = NULL, *lang = NULL; int64_t id = 0; mpv_get_property(player->mpv, title_prop, MPV_FORMAT_STRING, &title); mpv_get_property(player->mpv, lang_prop, MPV_FORMAT_STRING, &lang); mpv_get_property(player->mpv, id_prop, MPV_FORMAT_INT64, &id); if (title) { list[idx] = g_strdup_printf("Track %ld: %s", (long)id, title); } else if (lang) { list[idx] = g_strdup_printf("Track %ld: %s", (long)id, lang); } else { list[idx] = g_strdup_printf("Track %ld", (long)id); } if (title) mpv_free(title); if (lang) mpv_free(lang); idx++; } if (type) mpv_free(type); } return list; } void player_free_track_list(char **list, int count) { if (!list) return; for (int i = 0; i < count; i++) { g_free(list[i]); } g_free(list); } /* Playlist */ int player_get_playlist_count(Player *player) { if (!player || !player->mpv) return 0; int64_t count = 0; mpv_get_property(player->mpv, "playlist-count", MPV_FORMAT_INT64, &count); return (int)count; } int player_get_playlist_pos(Player *player) { if (!player || !player->mpv) return -1; int64_t pos = -1; mpv_get_property(player->mpv, "playlist-pos", MPV_FORMAT_INT64, &pos); return (int)pos; } void player_playlist_next(Player *player) { if (!player || !player->mpv) return; if (player_get_playlist_count(player) == 1) { player_seek_absolute(player, 0); } else { const char *cmd[] = {"playlist-next", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } } void player_playlist_prev(Player *player) { if (!player || !player->mpv) return; if (player_get_playlist_count(player) == 1) { player_seek_absolute(player, 0); } else { const char *cmd[] = {"playlist-prev", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } } void player_playlist_play_index(Player *player, int index) { if (!player || !player->mpv) return; char idx_str[32]; snprintf(idx_str, sizeof(idx_str), "%d", index); const char *cmd[] = {"playlist-play-index", idx_str, NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_playlist_remove(Player *player, int index) { if (!player || !player->mpv) return; char idx_str[32]; snprintf(idx_str, sizeof(idx_str), "%d", index); const char *cmd[] = {"playlist-remove", idx_str, NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_playlist_move(Player *player, int from, int to) { if (!player || !player->mpv) return; char from_str[32], to_str[32]; snprintf(from_str, sizeof(from_str), "%d", from); snprintf(to_str, sizeof(to_str), "%d", to); const char *cmd[] = {"playlist-move", from_str, to_str, NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_playlist_clear(Player *player) { if (!player || !player->mpv) return; const char *cmd[] = {"playlist-clear", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_playlist_shuffle(Player *player) { if (!player || !player->mpv) return; const char *cmd[] = {"playlist-shuffle", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_set_shuffle(Player *player, gboolean shuffle) { if (!player || !player->mpv) return; int val = shuffle ? 1 : 0; check_error(mpv_set_property_async(player->mpv, 0, "shuffle", MPV_FORMAT_FLAG, &val)); } void player_set_loop(Player *player, gboolean loop) { if (!player || !player->mpv) return; const char *val = loop ? "inf" : "no"; check_error(mpv_set_property_async(player->mpv, 0, "loop-playlist", MPV_FORMAT_STRING, &val)); } /* A-B Loop */ void player_set_ab_loop_a(Player *player) { if (!player || !player->mpv) return; double pos = player_get_position(player); mpv_set_property_async(player->mpv, 0, "ab-loop-a", MPV_FORMAT_DOUBLE, &pos); } void player_set_ab_loop_b(Player *player) { if (!player || !player->mpv) return; double pos = player_get_position(player); mpv_set_property_async(player->mpv, 0, "ab-loop-b", MPV_FORMAT_DOUBLE, &pos); } void player_clear_ab_loop(Player *player) { if (!player || !player->mpv) return; /* Reset by setting to "no" */ mpv_set_property_string(player->mpv, "ab-loop-a", "no"); mpv_set_property_string(player->mpv, "ab-loop-b", "no"); } double player_get_ab_loop_a(Player *player) { if (!player || !player->mpv) return -1.0; double a = -1.0; mpv_get_property(player->mpv, "ab-loop-a", MPV_FORMAT_DOUBLE, &a); return a; } double player_get_ab_loop_b(Player *player) { if (!player || !player->mpv) return -1.0; double b = -1.0; mpv_get_property(player->mpv, "ab-loop-b", MPV_FORMAT_DOUBLE, &b); return b; } /* Screenshot */ void player_screenshot(Player *player) { if (!player || !player->mpv) return; const char *cmd[] = {"screenshot", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } void player_screenshot_to_file(Player *player, const char *path) { if (!player || !player->mpv || !path) return; const char *cmd[] = {"screenshot-to-file", path, NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); } /* Aspect ratio */ void player_set_aspect_ratio(Player *player, const char *aspect) { if (!player || !player->mpv) return; mpv_set_property_string(player->mpv, "video-aspect-override", aspect ? aspect : "-1"); } /* Event handling */ void player_set_event_callback(Player *player, PlayerEventCallback callback, gpointer user_data) { if (!player) return; player->event_callback = callback; player->event_callback_data = user_data; } gboolean player_process_events(gpointer data) { Player *player = (Player *)data; if (!player || !player->mpv) return G_SOURCE_REMOVE; while (1) { mpv_event *event = mpv_wait_event(player->mpv, 0); if (event->event_id == MPV_EVENT_NONE) { break; } if (player->event_callback) { player->event_callback(player, event, player->event_callback_data); } if (event->event_id == MPV_EVENT_SHUTDOWN) { break; } } return G_SOURCE_REMOVE; } mpv_handle* player_get_mpv_handle(Player *player) { if (!player) return NULL; return player->mpv; } /* Property observation */ void player_observe_property(Player *player, const char *name, mpv_format format, guint64 reply_userdata) { if (!player || !player->mpv || !name) return; check_error(mpv_observe_property(player->mpv, reply_userdata, name, format)); } void player_unobserve_property(Player *player, guint64 reply_userdata) { if (!player || !player->mpv) return; mpv_unobserve_property(player->mpv, reply_userdata); }