From e8dc8edad7cdf194091f0479b70b154e872f57ef Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Mon, 20 Oct 2025 21:29:01 +0200 Subject: bt & main: Add optional player for device --- src/bt.cc | 288 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 286 insertions(+), 2 deletions(-) (limited to 'src/bt.cc') diff --git a/src/bt.cc b/src/bt.cc index 5675df2..ad05a87 100644 --- a/src/bt.cc +++ b/src/bt.cc @@ -3,6 +3,7 @@ #include "cfg.hh" #include "logger.hh" #include "looper.hh" +#include "sdbuscpp_no_throw_helper.hh" #include #include @@ -40,11 +41,27 @@ sdbus::Message& operator<<(sdbus::Message& msg, const Void& /* ignored */) { return msg; } +struct MediaPlayerTrack { + std::string Title; + std::string Artist; + std::string Album; + std::string Genre; + uint32_t NumberOfTracks; + uint32_t Duration; + std::string ImgHandle; +}; + } // namespace template <> struct sdbus::signature_of : sdbus::signature_of {}; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +SDBUSCPP_REGISTER_STRUCT_NO_THROW(MediaPlayerTrack, Title, Artist, Album, Genre, + NumberOfTracks, Duration, ImgHandle); +#pragma clang diagnostic pop + namespace bt { namespace { @@ -71,9 +88,19 @@ class IManager { virtual void update_adapter(sdbus::ObjectPath const& path) = 0; virtual void update_device(sdbus::ObjectPath const& path) = 0; + virtual void update_player(sdbus::ObjectPath const& path) = 0; + + virtual void add_player( + sdbus::ObjectPath const& path, + std::map const& properties) = 0; + virtual void remove_player(sdbus::ObjectPath const& path) = 0; + [[nodiscard]] virtual std::vector get_devices( - sdbus::ObjectPath const& adapter_path) = 0; + sdbus::ObjectPath const& adapter_path) const = 0; + + [[nodiscard]] + virtual Player* get_player(sdbus::ObjectPath const& player_path) const = 0; virtual void agent_request_pincode( sdbus::ObjectPath const& device, @@ -104,6 +131,7 @@ class IManager { const sdbus::ServiceName kService{"org.bluez"}; const sdbus::InterfaceName kDeviceInterface{"org.bluez.Device1"}; const sdbus::InterfaceName kAdapterInterface{"org.bluez.Adapter1"}; +const sdbus::InterfaceName kMediaPlayerInterface{"org.bluez.MediaPlayer1"}; const sdbus::InterfaceName kAgentInterface{"org.bluez.Agent1"}; const sdbus::InterfaceName kAgentManagerInterface{"org.bluez.AgentManager1"}; const sdbus::InterfaceName kPropertiesInterface{ @@ -195,6 +223,10 @@ class ObjectManagerProxy { if (it != interfaces_and_properties.end()) { manager_.add_agent_manager(object_path); } + it = interfaces_and_properties.find(kMediaPlayerInterface); + if (it != interfaces_and_properties.end()) { + manager_.add_player(object_path, it->second); + } } void interfaces_removed(sdbus::ObjectPath const& object_path, @@ -209,6 +241,8 @@ class ObjectManagerProxy { manager_.remove_adapter(object_path); } else if (interface == kAgentManagerInterface) { manager_.remove_agent_manager(object_path); + } else if (interface == kMediaPlayerInterface) { + manager_.remove_player(object_path); } } } @@ -330,11 +364,25 @@ class DeviceProxy : public BaseProxy, public Device { return connected_; } + [[nodiscard]] + Player* player() const override { + return manager_.get_player(primary_player_); + } + [[nodiscard]] sdbus::ObjectPath const& adapter() const { return adapter_; } + [[nodiscard]] + sdbus::ObjectPath const& primary_player() const { + return primary_player_; + } + + void set_primary_player(sdbus::ObjectPath const& path) { + primary_player_ = path; + } + private: void update(size_t field, sdbus::Variant const& value) override { switch (field) { @@ -376,6 +424,7 @@ class DeviceProxy : public BaseProxy, public Device { bool connected_{false}; bool paired_{false}; sdbus::ObjectPath adapter_; + sdbus::ObjectPath primary_player_; }; class AdapterProxy : public BaseProxy, public Adapter { @@ -550,6 +599,157 @@ class AdapterProxy : public BaseProxy, public Adapter { uint32_t pairable_timeout_{0}; }; +inline bool isActive(Player::Status status) { + switch (status) { + case Player::Status::kPlaying: + case Player::Status::kForwardSeek: + case Player::Status::kReverseSeek: + return true; + case Player::Status::kStopped: + case Player::Status::kPaused: + case Player::Status::kError: + return false; + } + std::unreachable(); +} + +class MediaPlayerProxy : public BaseProxy, public Player { + private: + static inline const std::map kNames{ + {sdbus::PropertyName{"Status"}, 0}, + {sdbus::PropertyName{"Track"}, 1}, + {sdbus::PropertyName{"Device"}, 2}, + {sdbus::PropertyName{"Name"}, 3}, + }; + + public: + MediaPlayerProxy( + IManager& manager, sdbus::IConnection& connection, + sdbus::ObjectPath const& path, + std::map const& properties) + : BaseProxy(connection, path, kMediaPlayerInterface, kNames), + manager_(manager) { + init(properties); + } + + [[nodiscard]] + Status status() const override { + return status_; + } + + [[nodiscard]] + std::string const& track_title() const override { + return track_.Title; + } + + [[nodiscard]] + std::string const& track_artist() const override { + return track_.Artist; + } + + [[nodiscard]] + std::string const& track_album() const override { + return track_.Album; + } + + void play() override { + switch (status_) { + case Status::kPlaying: + return; + case Status::kStopped: + case Status::kPaused: + case Status::kForwardSeek: + case Status::kReverseSeek: + case Status::kError: + break; + } + + getProxy() + .callMethodAsync("Play") + .onInterface(kMediaPlayerInterface) + .uponReplyInvoke([this](std::optional err) { + if (err.has_value()) { + manager_.logger().err(std::format("{}: Play {}: {}", name_, + std::string(err->getName()), + err->getMessage())); + } + }); + } + + void pause() override { + switch (status_) { + case Status::kStopped: + case Status::kPaused: + return; + case Status::kPlaying: + case Status::kForwardSeek: + case Status::kReverseSeek: + case Status::kError: + break; + } + + getProxy() + .callMethodAsync("Pause") + .onInterface(kMediaPlayerInterface) + .uponReplyInvoke([this](std::optional err) { + if (err.has_value()) { + manager_.logger().err(std::format("{}: Pause {}: {}", name_, + std::string(err->getName()), + err->getMessage())); + } + }); + } + + [[nodiscard]] + sdbus::ObjectPath const& device() const { + return device_; + } + + private: + void update(size_t field, sdbus::Variant const& value) override { + switch (field) { + case 0: { + auto tmp = value.get(); + if (tmp == "playing") { + status_ = Status::kPlaying; + } else if (tmp == "stopped") { + status_ = Status::kStopped; + } else if (tmp == "paused") { + status_ = Status::kPaused; + } else if (tmp == "forward-seek") { + status_ = Status::kForwardSeek; + } else if (tmp == "reverse-seek") { + status_ = Status::kReverseSeek; + } else { + status_ = Status::kError; + } + break; + } + case 1: + track_ = value.get(); + break; + case 2: + device_ = value.get(); + break; + case 3: + name_ = value.get(); + break; + default: + std::unreachable(); + } + } + + void notify_updated() override { + manager_.update_player(getProxy().getObjectPath()); + } + + IManager& manager_; + Status status_{Status::kError}; + MediaPlayerTrack track_{}; + sdbus::ObjectPath device_; + std::string name_; +}; + class AgentManagerProxy { public: AgentManagerProxy(sdbus::IConnection& connection, @@ -799,6 +999,20 @@ class ManagerImpl : public Manager, public IManager, looper::Hook { } } + void add_player(sdbus::ObjectPath const& path, + std::map const& + properties) override { + logger_.dbg(std::format("Add player: {}", std::string(path))); + auto [it, inserted] = players_.emplace( + path, + std::make_unique(*this, *conn_, path, properties)); + + auto it2 = devices_.find(it->second->device()); + if (it2 != devices_.end()) { + update_device_player(*it2->second); + } + } + void add_agent_manager(sdbus::ObjectPath const& path) override { logger_.dbg(std::format("Add agent manager: {}", std::string(path))); if (agent_manager_proxy_) @@ -835,6 +1049,19 @@ class ManagerImpl : public Manager, public IManager, looper::Hook { } } + void remove_player(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Remove player: {}", std::string(path))); + auto it = players_.find(path); + if (it == players_.end()) + return; + + auto it2 = devices_.find(it->second->device()); + players_.erase(it); + if (it2 != devices_.end()) { + update_device_player(*it2->second); + } + } + void remove_adapter(sdbus::ObjectPath const& path) override { logger_.dbg(std::format("Remove adapter: {}", std::string(path))); auto it = adapters_.find(path); @@ -877,8 +1104,26 @@ class ManagerImpl : public Manager, public IManager, looper::Hook { } } + void update_player(sdbus::ObjectPath const& path) override { + logger_.dbg(std::format("Update player: {}", std::string(path))); + + auto it = players_.find(path); + if (it == players_.end()) + return; + + auto it2 = devices_.find(it->second->device()); + if (it2 != devices_.end()) { + update_device_player(*it2->second); + + if (it2->second->player() == it->second.get()) { + delegate_.updated_player(*it2->second, *it->second); + } + } + } + + [[nodiscard]] std::vector get_devices( - sdbus::ObjectPath const& adapter_path) override { + sdbus::ObjectPath const& adapter_path) const override { std::vector ret; ret.reserve(devices_.size()); for (auto& pair : devices_) { @@ -889,6 +1134,17 @@ class ManagerImpl : public Manager, public IManager, looper::Hook { return ret; } + [[nodiscard]] + Player* get_player(sdbus::ObjectPath const& player_path) const override { + if (player_path.empty()) + return nullptr; + + auto it = players_.find(player_path); + if (it == players_.end()) + return nullptr; + return it->second.get(); + } + [[nodiscard]] logger::Logger& logger() override { return logger_; @@ -1033,6 +1289,33 @@ class ManagerImpl : public Manager, public IManager, looper::Hook { delegate_.new_adapter(nullptr); } + void update_device_player(DeviceProxy& device) { + if (device.adapter() != primary_adapter_) + return; + + sdbus::ObjectPath primary_player; + for (auto& pair : players_) { + if (pair.second->device() == device.getProxy().getObjectPath()) { + if (primary_player.empty() || isActive(pair.second->status())) { + primary_player = pair.first; + if (isActive(pair.second->status())) + break; + } + } + } + + if (device.primary_player() == primary_player) + return; + + device.set_primary_player(primary_player); + + if (primary_player.empty()) { + delegate_.removed_player(device); + } else { + delegate_.added_player(device, *device.player()); + } + } + logger::Logger& logger_; cfg::Config const& cfg_; looper::Looper& looper_; @@ -1043,6 +1326,7 @@ class ManagerImpl : public Manager, public IManager, looper::Hook { std::unique_ptr agent_; std::map> devices_; std::map> adapters_; + std::map> players_; sdbus::ObjectPath primary_adapter_; }; -- cgit v1.2.3-70-g09d2