#include "mpris.h" #include #include #include #include #define MPRIS_PATH "/org/mpris/MediaPlayer2" #define IFACE_ROOT "org.mpris.MediaPlayer2" #define IFACE_PLAYER "org.mpris.MediaPlayer2.Player" struct MprisPlayer { sd_bus *bus; sd_bus_slot *slot_root; sd_bus_slot *slot_player; char *identity; char *playback_status; double volume; int64_t position_us; int can_go_next; int can_go_previous; int can_play; int can_pause; int can_seek; char *track_id; char *title; char *artist; char *album; int64_t length_us; MprisCallback on_play; void *ud_play; MprisCallback on_pause; void *ud_pause; MprisCallback on_play_pause; void *ud_play_pause; MprisCallback on_stop; void *ud_stop; MprisCallback on_next; void *ud_next; MprisCallback on_previous; void *ud_previous; MprisSeekCallback on_seek; void *ud_seek; MprisSetPositionCallback on_set_position; void *ud_set_position; MprisVolumeCallback on_volume; void *ud_volume; }; /* ── Root interface ─────────────────────────────────────────────────────── */ static int handle_raise(sd_bus_message *m, void *ud, sd_bus_error *e) { return sd_bus_reply_method_return(m, ""); } static int handle_quit(sd_bus_message *m, void *ud, sd_bus_error *e) { return sd_bus_reply_method_return(m, ""); } static int get_identity(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "s", ((MprisPlayer *)ud)->identity); } static int get_false(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", 0); } static int get_empty_strv(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { int r = sd_bus_message_open_container(reply, 'a', "s"); if (r < 0) return r; return sd_bus_message_close_container(reply); } static const sd_bus_vtable root_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("Raise", "", "", handle_raise, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Quit", "", "", handle_quit, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_PROPERTY("CanQuit", "b", get_false, 0, 0), SD_BUS_PROPERTY("CanRaise", "b", get_false, 0, 0), SD_BUS_PROPERTY("HasTrackList", "b", get_false, 0, 0), SD_BUS_PROPERTY("Identity", "s", get_identity, 0, 0), SD_BUS_PROPERTY("SupportedUriSchemes", "as", get_empty_strv, 0, 0), SD_BUS_PROPERTY("SupportedMimeTypes", "as", get_empty_strv, 0, 0), SD_BUS_VTABLE_END }; /* ── Player interface ───────────────────────────────────────────────────── */ static int handle_play(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; if (p->on_play) p->on_play(p->ud_play); return sd_bus_reply_method_return(m, ""); } static int handle_pause(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; if (p->on_pause) p->on_pause(p->ud_pause); return sd_bus_reply_method_return(m, ""); } static int handle_play_pause(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; if (p->on_play_pause) p->on_play_pause(p->ud_play_pause); return sd_bus_reply_method_return(m, ""); } static int handle_stop(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; if (p->on_stop) p->on_stop(p->ud_stop); return sd_bus_reply_method_return(m, ""); } static int handle_next(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; if (p->on_next) p->on_next(p->ud_next); return sd_bus_reply_method_return(m, ""); } static int handle_previous(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; if (p->on_previous) p->on_previous(p->ud_previous); return sd_bus_reply_method_return(m, ""); } static int handle_seek(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; int64_t offset; int r = sd_bus_message_read(m, "x", &offset); if (r < 0) return r; if (p->on_seek) p->on_seek(offset, p->ud_seek); return sd_bus_reply_method_return(m, ""); } static int handle_set_position(sd_bus_message *m, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; const char *track_id; int64_t position; int r = sd_bus_message_read(m, "ox", &track_id, &position); if (r < 0) return r; if (p->on_set_position) p->on_set_position(track_id, position, p->ud_set_position); return sd_bus_reply_method_return(m, ""); } static int handle_open_uri(sd_bus_message *m, void *ud, sd_bus_error *e) { return sd_bus_reply_method_return(m, ""); } static int get_playback_status(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "s", ((MprisPlayer *)ud)->playback_status); } static int get_loop_status(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "s", "None"); } static int get_shuffle(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", 0); } static int get_metadata(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; int r; r = sd_bus_message_open_container(reply, 'a', "{sv}"); if (r < 0) return r; /* mpris:trackid is mandatory */ const char *trackid = p->track_id ? p->track_id : "/org/mpris/MediaPlayer2/TrackList/NoTrack"; r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", "mpris:trackid"); if (r < 0) return r; r = sd_bus_message_open_container(reply, 'v', "o"); if (r < 0) return r; r = sd_bus_message_append(reply, "o", trackid); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; if (p->title) { r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", "xesam:title"); if (r < 0) return r; r = sd_bus_message_open_container(reply, 'v', "s"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", p->title); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; } /* xesam:artist is an array of strings */ if (p->artist) { r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", "xesam:artist"); if (r < 0) return r; r = sd_bus_message_open_container(reply, 'v', "as"); if (r < 0) return r; r = sd_bus_message_open_container(reply, 'a', "s"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", p->artist); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; } if (p->album) { r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", "xesam:album"); if (r < 0) return r; r = sd_bus_message_open_container(reply, 'v', "s"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", p->album); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; } if (p->length_us > 0) { r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r; r = sd_bus_message_append(reply, "s", "mpris:length"); if (r < 0) return r; r = sd_bus_message_open_container(reply, 'v', "x"); if (r < 0) return r; r = sd_bus_message_append(reply, "x", p->length_us); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; r = sd_bus_message_close_container(reply); if (r < 0) return r; } return sd_bus_message_close_container(reply); } static int get_volume(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "d", ((MprisPlayer *)ud)->volume); } static int set_volume(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *value, void *ud, sd_bus_error *e) { MprisPlayer *p = ud; double v; int r = sd_bus_message_read(value, "d", &v); if (r < 0) return r; p->volume = v; if (p->on_volume) p->on_volume(v, p->ud_volume); return 1; /* signal that the value changed so PropertiesChanged is emitted */ } static int get_position(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "x", ((MprisPlayer *)ud)->position_us); } static int get_rate(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "d", 1.0); } static int get_can_go_next(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_go_next); } static int get_can_go_previous(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_go_previous); } static int get_can_play(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_play); } static int get_can_pause(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_pause); } static int get_can_seek(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", ((MprisPlayer *)ud)->can_seek); } static int get_can_control(sd_bus *bus, const char *path, const char *iface, const char *prop, sd_bus_message *reply, void *ud, sd_bus_error *e) { return sd_bus_message_append(reply, "b", 1); } static const sd_bus_vtable player_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("Play", "", "", handle_play, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Pause", "", "", handle_pause, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("PlayPause", "", "", handle_play_pause, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Stop", "", "", handle_stop, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Next", "", "", handle_next, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Previous", "", "", handle_previous, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Seek", "x", "", handle_seek, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("SetPosition", "ox", "", handle_set_position, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("OpenUri", "s", "", handle_open_uri, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_SIGNAL("Seeked", "x", 0), SD_BUS_PROPERTY ("PlaybackStatus", "s", get_playback_status, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("LoopStatus", "s", get_loop_status, 0, 0), SD_BUS_PROPERTY ("Shuffle", "b", get_shuffle, 0, 0), SD_BUS_PROPERTY ("Metadata", "a{sv}", get_metadata, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_WRITABLE_PROPERTY("Volume", "d", get_volume, set_volume, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("Position", "x", get_position, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION), SD_BUS_PROPERTY ("Rate", "d", get_rate, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("MinimumRate", "d", get_rate, 0, 0), SD_BUS_PROPERTY ("MaximumRate", "d", get_rate, 0, 0), SD_BUS_PROPERTY ("CanGoNext", "b", get_can_go_next, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("CanGoPrevious", "b", get_can_go_previous, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("CanPlay", "b", get_can_play, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("CanPause", "b", get_can_pause, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("CanSeek", "b", get_can_seek, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), SD_BUS_PROPERTY ("CanControl", "b", get_can_control, 0, 0), SD_BUS_VTABLE_END }; /* ── Public API ─────────────────────────────────────────────────────────── */ MprisPlayer *mpris_player_create(const char *player_name, const char *identity) { MprisPlayer *p = calloc(1, sizeof(MprisPlayer)); if (!p) return NULL; p->identity = strdup(identity); p->playback_status = strdup("Stopped"); p->volume = 1.0; p->can_play = 1; p->can_pause = 1; int r; r = sd_bus_open_user(&p->bus); if (r < 0) { fprintf(stderr, "mpris: D-Bus connect failed: %s\n", strerror(-r)); goto fail; } char bus_name[256]; snprintf(bus_name, sizeof(bus_name), "org.mpris.MediaPlayer2.%s", player_name); r = sd_bus_request_name(p->bus, bus_name, 0); if (r < 0) { fprintf(stderr, "mpris: could not acquire %s: %s\n", bus_name, strerror(-r)); goto fail; } r = sd_bus_add_object_vtable(p->bus, &p->slot_root, MPRIS_PATH, IFACE_ROOT, root_vtable, p); if (r < 0) { fprintf(stderr, "mpris: register root interface failed: %s\n", strerror(-r)); goto fail; } r = sd_bus_add_object_vtable(p->bus, &p->slot_player, MPRIS_PATH, IFACE_PLAYER, player_vtable, p); if (r < 0) { fprintf(stderr, "mpris: register player interface failed: %s\n", strerror(-r)); goto fail; } return p; fail: mpris_player_destroy(p); return NULL; } void mpris_player_destroy(MprisPlayer *p) { if (!p) return; sd_bus_slot_unref(p->slot_player); sd_bus_slot_unref(p->slot_root); sd_bus_unref(p->bus); free(p->identity); free(p->playback_status); free(p->track_id); free(p->title); free(p->artist); free(p->album); free(p); } #define EMIT_PLAYER(p, ...) \ sd_bus_emit_properties_changed((p)->bus, MPRIS_PATH, IFACE_PLAYER, __VA_ARGS__, NULL) void mpris_set_playback_status(MprisPlayer *p, const char *status) { free(p->playback_status); p->playback_status = strdup(status); EMIT_PLAYER(p, "PlaybackStatus"); } void mpris_set_metadata(MprisPlayer *p, const char *track_id, const char *title, const char *artist, const char *album, int64_t length_us) { free(p->track_id); p->track_id = track_id ? strdup(track_id) : NULL; free(p->title); p->title = title ? strdup(title) : NULL; free(p->artist); p->artist = artist ? strdup(artist) : NULL; free(p->album); p->album = album ? strdup(album) : NULL; p->length_us = length_us; EMIT_PLAYER(p, "Metadata"); } void mpris_set_volume(MprisPlayer *p, double volume) { p->volume = volume; EMIT_PLAYER(p, "Volume"); } void mpris_set_position(MprisPlayer *p, int64_t position_us) { p->position_us = position_us; /* Position uses EMITS_INVALIDATION — clients poll it or watch the Seeked signal */ } void mpris_set_can_go_next(MprisPlayer *p, int v) { p->can_go_next = v; EMIT_PLAYER(p, "CanGoNext"); } void mpris_set_can_go_previous(MprisPlayer *p, int v) { p->can_go_previous = v; EMIT_PLAYER(p, "CanGoPrevious"); } void mpris_set_can_play(MprisPlayer *p, int v) { p->can_play = v; EMIT_PLAYER(p, "CanPlay"); } void mpris_set_can_pause(MprisPlayer *p, int v) { p->can_pause = v; EMIT_PLAYER(p, "CanPause"); } void mpris_set_can_seek(MprisPlayer *p, int v) { p->can_seek = v; EMIT_PLAYER(p, "CanSeek"); } void mpris_emit_seeked(MprisPlayer *p, int64_t position_us) { p->position_us = position_us; sd_bus_emit_signal(p->bus, MPRIS_PATH, IFACE_PLAYER, "Seeked", "x", position_us); } void mpris_on_play (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_play = cb; p->ud_play = ud; } void mpris_on_pause (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_pause = cb; p->ud_pause = ud; } void mpris_on_play_pause (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_play_pause = cb; p->ud_play_pause = ud; } void mpris_on_stop (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_stop = cb; p->ud_stop = ud; } void mpris_on_next (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_next = cb; p->ud_next = ud; } void mpris_on_previous (MprisPlayer *p, MprisCallback cb, void *ud) { p->on_previous = cb; p->ud_previous = ud; } void mpris_on_seek (MprisPlayer *p, MprisSeekCallback cb, void *ud) { p->on_seek = cb; p->ud_seek = ud; } void mpris_on_set_position(MprisPlayer *p, MprisSetPositionCallback cb, void *ud) { p->on_set_position = cb; p->ud_set_position = ud; } void mpris_on_volume (MprisPlayer *p, MprisVolumeCallback cb, void *ud) { p->on_volume = cb; p->ud_volume = ud; } int mpris_process(MprisPlayer *p) { return sd_bus_process(p->bus, NULL); }