2026-05-04 19:50:59 +03:00

410 lines
19 KiB
C

#include "mpris.h"
#include <systemd/sd-bus.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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);
}