mpris mostly works, still need to init the state though

This commit is contained in:
2026-02-03 11:22:57 +01:00
parent 9c70065bf6
commit b9f5eea4af
10 changed files with 170 additions and 22 deletions

View File

@@ -27,8 +27,8 @@ class Bar : public Gtk::Window {
Clock clock; Clock clock;
Date date; Date date;
WebWidget homeAssistant{"\ue88a", "Home Assistant", "https://home.rivercry.com"}; WebWidget homeAssistant{"\uf024", "Home Assistant", "https://home.rivercry.com"};
ControlCenter controlCenter{"\ue88a", "Control Center"}; ControlCenter controlCenter{"\ue062", "Control Center"};
std::shared_ptr<TrayWidget> trayWidget = nullptr; std::shared_ptr<TrayWidget> trayWidget = nullptr;
std::shared_ptr<VolumeWidget> volumeWidget = nullptr; std::shared_ptr<VolumeWidget> volumeWidget = nullptr;

View File

@@ -16,6 +16,7 @@ struct MprisPlayer2Message {
std::string title; std::string title;
std::vector<std::string> artist; std::vector<std::string> artist;
std::string artwork_url; std::string artwork_url;
std::string track_id;
int64_t length_ms; int64_t length_ms;
std::function<void()> play_pause; std::function<void()> play_pause;

View File

@@ -26,10 +26,13 @@ class MprisController {
void next_song(); void next_song();
void previous_song(); void previous_song();
void emit_seeked(int64_t position_us); void emit_seeked(int64_t position_us);
void set_position(const std::string &track_id, int64_t position_us);
sigc::signal<void(const MprisPlayer2Message &)> &signal_mpris_updated(); sigc::signal<void(const MprisPlayer2Message &)> &signal_mpris_updated();
sigc::signal<void(PlaybackStatus)> &signal_playback_status_changed(); sigc::signal<void(PlaybackStatus)> &signal_playback_status_changed();
sigc::signal<void(int64_t)> &signal_playback_position_changed(); sigc::signal<void(int64_t)> &signal_playback_position_changed();
sigc::signal<void(const std::string &)> &signal_player_registered();
sigc::signal<void(const std::string &)> &signal_player_deregistered();
private: private:
MprisController(); MprisController();
@@ -43,13 +46,22 @@ class MprisController {
Glib::RefPtr<Gio::DBus::Connection> m_connection; Glib::RefPtr<Gio::DBus::Connection> m_connection;
Glib::RefPtr<Gio::DBus::Proxy> m_proxy; Glib::RefPtr<Gio::DBus::Proxy> m_proxy;
Glib::RefPtr<Gio::DBus::Proxy> m_dbus_proxy;
std::string m_player_bus_name = "org.mpris.MediaPlayer2.spotify";
sigc::signal<void(const MprisPlayer2Message &)> mprisUpdatedSignal; sigc::signal<void(const MprisPlayer2Message &)> mprisUpdatedSignal;
sigc::signal<void(PlaybackStatus)> playbackStatusChangedSignal; sigc::signal<void(PlaybackStatus)> playbackStatusChangedSignal;
sigc::signal<void(int64_t)> playbackPositionChangedSignal; sigc::signal<void(int64_t)> playbackPositionChangedSignal;
sigc::signal<void(const std::string &)> playerRegisteredSignal;
sigc::signal<void(const std::string &)> playerDeregisteredSignal;
void on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &result); void on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &result);
void signalNotification(); void signalNotification();
void on_dbus_signal(const Glib::ustring &sender_name,
const Glib::ustring &signal_name,
const Glib::VariantContainerBase &parameters);
void handle_player_registered(const std::string &bus_name);
void handle_player_deregistered(const std::string &bus_name);
// Called when the song changes // Called when the song changes
void on_properties_changed(const Gio::DBus::Proxy::MapChangedProperties &changed_properties, void on_properties_changed(const Gio::DBus::Proxy::MapChangedProperties &changed_properties,

View File

@@ -19,6 +19,8 @@ class MediaControlWidget : public Gtk::Box {
int64_t currentPositionUs = 0; int64_t currentPositionUs = 0;
int64_t totalLengthUs = 0; int64_t totalLengthUs = 0;
sigc::connection seekTimerConnection; sigc::connection seekTimerConnection;
bool suppressSeekSignal = false;
std::string currentTrackId;
void setCurrentPosition(int64_t position_us); void setCurrentPosition(int64_t position_us);
void setTotalLength(int64_t length_us); void setTotalLength(int64_t length_us);

View File

@@ -9,7 +9,7 @@
#include "gdkmm/monitor.h" #include "gdkmm/monitor.h"
#include "gtkmm/window.h" #include "gtkmm/window.h"
#define DEFAULT_NOTIFICATION_TIMEOUT 5000 #define DEFAULT_NOTIFICATION_TIMEOUT 6700
class BaseNotification : public Gtk::Window { class BaseNotification : public Gtk::Window {

View File

@@ -21,6 +21,11 @@ window {
font-family: var(--text-font); font-family: var(--text-font);
} }
.material-icons {
font-family: var(--icon-font-material);
font-size: 18px;
}
popover { popover {
margin-top: 4px; margin-top: 4px;
font-family: var(--text-font); font-family: var(--text-font);

View File

@@ -5,6 +5,8 @@ Popover::Popover(const std::string icon, std::string name): Button(icon) {
set_name(name); set_name(name);
this->add_css_class("material-icons");
popover = new Gtk::Popover(); popover = new Gtk::Popover();
popover->set_parent(*this); popover->set_parent(*this);
popover->set_autohide(true); popover->set_autohide(true);

View File

@@ -32,6 +32,14 @@ sigc::signal<void(int64_t)> &MprisController::signal_playback_position_changed()
return playbackPositionChangedSignal; return playbackPositionChangedSignal;
} }
sigc::signal<void(const std::string &)> &MprisController::signal_player_registered() {
return playerRegisteredSignal;
}
sigc::signal<void(const std::string &)> &MprisController::signal_player_deregistered() {
return playerDeregisteredSignal;
}
void MprisController::on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &result) { void MprisController::on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &result) {
if (!result) { if (!result) {
spdlog::error("DBus Connection Error: null async result"); spdlog::error("DBus Connection Error: null async result");
@@ -40,22 +48,32 @@ void MprisController::on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &res
try { try {
m_connection = Gio::DBus::Connection::get_finish(result); m_connection = Gio::DBus::Connection::get_finish(result);
std::string player_bus_name = "org.mpris.MediaPlayer2.spotify"; if (!m_dbus_proxy) {
m_dbus_proxy = Gio::DBus::Proxy::create_sync(
m_connection,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus");
m_proxy = Gio::DBus::Proxy::create_sync( if (m_dbus_proxy) {
m_connection, m_dbus_proxy->signal_signal().connect(
player_bus_name, // The Bus Name sigc::mem_fun(*this, &MprisController::on_dbus_signal));
"/org/mpris/MediaPlayer2", // The Object Path
"org.mpris.MediaPlayer2.Player" // The Interface
);
if (m_proxy) { try {
spdlog::info("Connected to: {}", player_bus_name); auto list_names_result = m_dbus_proxy->call_sync("ListNames");
auto names_variant = Glib::VariantBase::cast_dynamic<
Glib::Variant<std::vector<Glib::ustring>>>(list_names_result.get_child(0));
signalNotification(); for (const auto &name : names_variant.get()) {
const std::string bus_name = name;
m_proxy->signal_properties_changed().connect( if (bus_name.rfind("org.mpris.MediaPlayer2.", 0) == 0) {
sigc::mem_fun(*this, &MprisController::on_properties_changed)); handle_player_registered(bus_name);
}
}
} catch (const Glib::Error &ex) {
spdlog::error("DBus ListNames Error: {}", ex.what());
}
}
} }
} catch (const Glib::Error &ex) { } catch (const Glib::Error &ex) {
@@ -63,6 +81,66 @@ void MprisController::on_bus_connected(const Glib::RefPtr<Gio::AsyncResult> &res
} }
} }
void MprisController::on_dbus_signal(const Glib::ustring &,
const Glib::ustring &signal_name,
const Glib::VariantContainerBase &parameters) {
if (signal_name != "NameOwnerChanged") {
return;
}
auto name_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(0));
auto old_owner_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(1));
auto new_owner_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(parameters.get_child(2));
const std::string bus_name = name_var.get();
if (bus_name.rfind("org.mpris.MediaPlayer2.", 0) != 0) {
return;
}
const bool had_owner = !old_owner_var.get().empty();
const bool has_owner = !new_owner_var.get().empty();
if (!had_owner && has_owner) {
handle_player_registered(bus_name);
} else if (had_owner && !has_owner) {
handle_player_deregistered(bus_name);
}
}
void MprisController::handle_player_registered(const std::string &bus_name) {
spdlog::info("MPRIS player registered: {}", bus_name);
if (bus_name == m_player_bus_name && !m_proxy && m_connection) {
try {
m_proxy = Gio::DBus::Proxy::create_sync(
m_connection,
m_player_bus_name,
"/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player");
if (m_proxy) {
m_proxy->signal_properties_changed().connect(
sigc::mem_fun(*this, &MprisController::on_properties_changed));
signalNotification();
}
} catch (const Glib::Error &ex) {
spdlog::error("DBus Connection Error: {}", ex.what());
}
}
playerRegisteredSignal.emit(bus_name);
}
void MprisController::handle_player_deregistered(const std::string &bus_name) {
spdlog::info("MPRIS player deregistered: {}", bus_name);
if (bus_name == m_player_bus_name) {
m_proxy.reset();
}
playerDeregisteredSignal.emit(bus_name);
}
void MprisController::signalNotification() { void MprisController::signalNotification() {
if (!m_proxy) { if (!m_proxy) {
return; return;
@@ -89,7 +167,7 @@ void MprisController::signalNotification() {
metadata_map = variant_dict.get(); metadata_map = variant_dict.get();
std::string title, artwork_url; std::string title, artwork_url, track_id;
std::vector<std::string> artist; std::vector<std::string> artist;
if (metadata_map.count("xesam:title")) { if (metadata_map.count("xesam:title")) {
@@ -124,6 +202,17 @@ void MprisController::signalNotification() {
} }
} }
if (metadata_map.count("mpris:trackid")) {
const auto &track_base = metadata_map["mpris:trackid"];
if (track_base.is_of_type(Glib::VariantType("o"))) {
auto track_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(track_base);
track_id = track_var.get();
} else if (track_base.is_of_type(Glib::VariantType("s"))) {
auto track_var = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(track_base);
track_id = track_var.get();
}
}
int64_t length_us = 0; int64_t length_us = 0;
if (metadata_map.count("mpris:length")) { if (metadata_map.count("mpris:length")) {
const auto &length_base = metadata_map["mpris:length"]; const auto &length_base = metadata_map["mpris:length"];
@@ -140,10 +229,11 @@ void MprisController::signalNotification() {
mpris.title = StringHelper::trimToSize(title, 30); mpris.title = StringHelper::trimToSize(title, 30);
mpris.artist = artist; mpris.artist = artist;
mpris.artwork_url = artwork_url; mpris.artwork_url = artwork_url;
mpris.track_id = track_id;
mpris.play_pause = [this]() { this->toggle_play(); }; mpris.play_pause = [this]() { this->toggle_play(); };
mpris.next = [this]() { this->next_song(); }; mpris.next = [this]() { this->next_song(); };
mpris.previous = [this]() { this->previous_song(); }; mpris.previous = [this]() { this->previous_song(); };
mpris.length_ms = length_us; // Convert microseconds to milliseconds mpris.length_ms = length_us / 1000; // Convert microseconds to milliseconds
mprisUpdatedSignal.emit(mpris); mprisUpdatedSignal.emit(mpris);
} }
@@ -177,7 +267,7 @@ void MprisController::on_properties_changed(const Gio::DBus::Proxy::MapChangedPr
spdlog::info("Position changed to: {}", position); spdlog::info("Position changed to: {}", position);
playbackPositionChangedSignal.emit(static_cast<int64_t>(position)); playbackPositionChangedSignal.emit(static_cast<int64_t>(position));
} }
} }
} }
void MprisController::previous_song() { void MprisController::previous_song() {
@@ -199,6 +289,8 @@ void MprisController::next_song() {
} }
void MprisController::emit_seeked(int64_t position_us) { void MprisController::emit_seeked(int64_t position_us) {
spdlog::debug("Seeking to position: {}", position_us);
if (!m_proxy) { if (!m_proxy) {
return; return;
} }
@@ -212,3 +304,20 @@ void MprisController::emit_seeked(int64_t position_us) {
spdlog::error("Error seeking: {}", ex.what()); spdlog::error("Error seeking: {}", ex.what());
} }
} }
void MprisController::set_position(const std::string &track_id, int64_t position_us) {
if (!m_proxy || track_id.empty()) {
return;
}
try {
Glib::VariantContainerBase params = Glib::VariantContainerBase::create_tuple({
Glib::Variant<Glib::DBusObjectPathString>::create(track_id),
Glib::Variant<gint64>::create(position_us)
});
m_proxy->call("SetPosition", params);
} catch (const Glib::Error &ex) {
spdlog::error("Error setting position: {}", ex.what());
}
}

View File

@@ -60,10 +60,20 @@ MediaControlWidget::MediaControlWidget()
this->seekBar.add_css_class("control-center-seek-bar"); this->seekBar.add_css_class("control-center-seek-bar");
this->seekBar.signal_value_changed().connect([this]() { this->seekBar.signal_value_changed().connect([this]() {
if (this->suppressSeekSignal || this->totalLengthUs <= 0) {
return;
}
double fraction = this->seekBar.get_value() / 100.0; double fraction = this->seekBar.get_value() / 100.0;
int64_t new_position_us = int64_t new_position_us =
static_cast<int64_t>(fraction * static_cast<double>(this->totalLengthUs)); static_cast<int64_t>(fraction * static_cast<double>(this->totalLengthUs));
this->mprisController->emit_seeked(new_position_us - this->currentPositionUs); // in us if (new_position_us == this->currentPositionUs) {
return;
}
if (!this->currentTrackId.empty()) {
this->mprisController->set_position(this->currentTrackId, new_position_us);
} else {
this->mprisController->emit_seeked(new_position_us - this->currentPositionUs); // in us
}
this->resetSeekTimer(new_position_us); this->resetSeekTimer(new_position_us);
}); });
@@ -126,12 +136,13 @@ void MediaControlWidget::onSpotifyMprisUpdated(const MprisPlayer2Message &messag
} }
this->artistLabel.set_text(artistText); this->artistLabel.set_text(artistText);
this->titleLabel.set_text(message.title); this->titleLabel.set_text(message.title);
this->currentTrackId = message.track_id;
if (auto texture = TextureCacheService::getInstance()->getTexture(message.artwork_url)) { if (auto texture = TextureCacheService::getInstance()->getTexture(message.artwork_url)) {
this->backgroundImage.set_paintable(texture); this->backgroundImage.set_paintable(texture);
} }
this->setTotalLength(message.length_ms); this->setTotalLength(message.length_ms * 1000);
this->setCurrentPosition(0); this->setCurrentPosition(0);
this->resetSeekTimer(0); this->resetSeekTimer(0);
} }
@@ -144,7 +155,9 @@ void MediaControlWidget::setCurrentPosition(int64_t position_us) {
std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds)); std::to_string(minutes) + ":" + (seconds < 10 ? "0" : "") + std::to_string(seconds));
if (totalLengthUs > 0) { if (totalLengthUs > 0) {
double fraction = static_cast<double>(currentPositionUs) / static_cast<double>(totalLengthUs); double fraction = static_cast<double>(currentPositionUs) / static_cast<double>(totalLengthUs);
this->suppressSeekSignal = true;
this->seekBar.set_value(fraction * 100); this->seekBar.set_value(fraction * 100);
this->suppressSeekSignal = false;
} }
} }

View File

@@ -33,7 +33,11 @@ SpotifyNotification::SpotifyNotification(uint64_t notificationId, std::shared_pt
title_label->set_ellipsize(Pango::EllipsizeMode::END); title_label->set_ellipsize(Pango::EllipsizeMode::END);
auto artistLabel = Gtk::make_managed<Gtk::Label>(); auto artistLabel = Gtk::make_managed<Gtk::Label>();
artistLabel->set_text(StringHelper::trimToSize(mpris.artist[0], 30)); if (!mpris.artist.empty()) {
artistLabel->set_text(StringHelper::trimToSize(mpris.artist[0], 30));
} else {
artistLabel->set_text("Unknown Artist");
}
artistLabel->set_hexpand(true); artistLabel->set_hexpand(true);
artistLabel->set_halign(Gtk::Align::CENTER); artistLabel->set_halign(Gtk::Align::CENTER);