diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-10-20 21:29:01 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-10-20 21:29:01 +0200 |
| commit | e8dc8edad7cdf194091f0479b70b154e872f57ef (patch) | |
| tree | 5431680ce100812b9b3bea32a7847e7dcfdcf29d | |
| parent | 0687ec31d1d75500beaee0e983ebf73d7c4517f7 (diff) | |
bt & main: Add optional player for device
| -rw-r--r-- | src/bt.cc | 288 | ||||
| -rw-r--r-- | src/bt.hh | 47 | ||||
| -rw-r--r-- | src/main.cc | 54 | ||||
| -rw-r--r-- | src/sdbuscpp_no_throw_helper.hh | 61 |
4 files changed, 448 insertions, 2 deletions
@@ -3,6 +3,7 @@ #include "cfg.hh" #include "logger.hh" #include "looper.hh" +#include "sdbuscpp_no_throw_helper.hh" #include <chrono> #include <cstddef> @@ -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<Void> : sdbus::signature_of<void> {}; +#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<sdbus::PropertyName, sdbus::Variant> const& properties) = 0; + virtual void remove_player(sdbus::ObjectPath const& path) = 0; + [[nodiscard]] virtual std::vector<Device*> 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); } } } @@ -331,10 +365,24 @@ class DeviceProxy : public BaseProxy, public Device { } [[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<sdbus::PropertyName, size_t> 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<sdbus::PropertyName, sdbus::Variant> 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<sdbus::Error> 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<sdbus::Error> 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<std::string>(); + 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<MediaPlayerTrack>(); + break; + case 2: + device_ = value.get<sdbus::ObjectPath>(); + break; + case 3: + name_ = value.get<std::string>(); + 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<sdbus::PropertyName, sdbus::Variant> const& + properties) override { + logger_.dbg(std::format("Add player: {}", std::string(path))); + auto [it, inserted] = players_.emplace( + path, + std::make_unique<MediaPlayerProxy>(*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<Device*> get_devices( - sdbus::ObjectPath const& adapter_path) override { + sdbus::ObjectPath const& adapter_path) const override { std::vector<Device*> ret; ret.reserve(devices_.size()); for (auto& pair : devices_) { @@ -890,6 +1135,17 @@ class ManagerImpl : public Manager, public IManager, looper::Hook { } [[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<AgentObject> agent_; std::map<sdbus::ObjectPath, std::unique_ptr<DeviceProxy>> devices_; std::map<sdbus::ObjectPath, std::unique_ptr<AdapterProxy>> adapters_; + std::map<sdbus::ObjectPath, std::unique_ptr<MediaPlayerProxy>> players_; sdbus::ObjectPath primary_adapter_; }; @@ -23,6 +23,7 @@ class Config; namespace bt { class Device; +class Player; class Adapter { public: @@ -84,12 +85,51 @@ class Device { [[nodiscard]] virtual bool connected() const = 0; + // Returns null if there are no players. + // Otherwise, returns the "primary" player. + [[nodiscard]] + virtual Player* player() const = 0; + protected: Device() = default; Device(Device const&) = delete; Device& operator=(Device const&) = delete; }; +class Player { + public: + virtual ~Player() = default; + + enum class Status : uint8_t { + kPlaying, + kStopped, + kPaused, + kForwardSeek, + kReverseSeek, + kError, + }; + + [[nodiscard]] + virtual Status status() const = 0; + + [[nodiscard]] + virtual std::string const& track_title() const = 0; + + [[nodiscard]] + virtual std::string const& track_artist() const = 0; + + [[nodiscard]] + virtual std::string const& track_album() const = 0; + + virtual void play() = 0; + virtual void pause() = 0; + + protected: + Player() = default; + Player(Player const&) = delete; + Player& operator=(Player const&) = delete; +}; + class Manager { public: virtual ~Manager() = default; @@ -110,6 +150,13 @@ class Manager { // Called when device changes a property. virtual void updated_device(Device& /* device */) {} + // Called when a new player is added to a device. + virtual void added_player(Device& /* device */, Player& /* player */) {} + // Called when a player was removed from a device. + virtual void removed_player(Device& /* device */) {} + // Called when a device player changes a property. + virtual void updated_player(Device& /* device */, Player& /* player */) {} + virtual void agent_request_pincode( Device& device, std::function<void(std::optional<std::string>)> callback) = 0; diff --git a/src/main.cc b/src/main.cc index ce20aee..83fd905 100644 --- a/src/main.cc +++ b/src/main.cc @@ -41,6 +41,7 @@ class Api { }; const std::string_view kSignalUpdateAdapter("controller/update"); +const std::string kSignalUpdateDevicePrefix("device/update/"); class Signaler { public: @@ -173,10 +174,44 @@ class HttpServerDelegate : public http::Server::Delegate { json_writer_->start_object(); json_writer_->key("name"); json_writer_->value(device.name()); + json_writer_->key("address"); + json_writer_->value(device.address()); json_writer_->key("paired"); json_writer_->value(device.paired()); json_writer_->key("connected"); json_writer_->value(device.connected()); + + json_writer_->key("playing"); + if (auto* player = device.player()) { + json_writer_->start_object(); + json_writer_->key("status"); + switch (player->status()) { + case bt::Player::Status::kPlaying: + case bt::Player::Status::kForwardSeek: + case bt::Player::Status::kReverseSeek: + json_writer_->value("playing"); + break; + case bt::Player::Status::kStopped: + json_writer_->value("stopped"); + break; + case bt::Player::Status::kPaused: + json_writer_->value("paused"); + break; + case bt::Player::Status::kError: + json_writer_->value("error"); + break; + } + json_writer_->key("title"); + json_writer_->value(player->track_title()); + json_writer_->key("album"); + json_writer_->value(player->track_album()); + json_writer_->key("artist"); + json_writer_->value(player->track_artist()); + json_writer_->end_object(); + } else { + json_writer_->value(nullptr); + } + json_writer_->end_object(); } @@ -240,6 +275,25 @@ class BluetoothManagerDelegate : public bt::Manager::Delegate, public Api { signaler_.send(kSignalUpdateAdapter); } + void added_player(bt::Device& device, bt::Player& /* player */) override { + logger_.info(std::format("New player for {}", device.name())); + + if (adapter_) + signaler_.send(kSignalUpdateDevicePrefix + device.address()); + } + + void removed_player(bt::Device& device) override { + logger_.info(std::format("Remove player for {}", device.name())); + + if (adapter_) + signaler_.send(kSignalUpdateDevicePrefix + device.address()); + } + + void updated_player(bt::Device& device, bt::Player& /* player */) override { + if (adapter_) + signaler_.send(kSignalUpdateDevicePrefix + device.address()); + } + void agent_request_pincode( bt::Device& device, std::function<void(std::optional<std::string>)> callback) override { diff --git a/src/sdbuscpp_no_throw_helper.hh b/src/sdbuscpp_no_throw_helper.hh new file mode 100644 index 0000000..4696895 --- /dev/null +++ b/src/sdbuscpp_no_throw_helper.hh @@ -0,0 +1,61 @@ +#ifndef SDBUSCPP_NO_THROW_HELPER_HH +#define SDBUSCPP_NO_THROW_HELPER_HH + +// SDBUSCPP_ENABLE_RELAXED_DICT2STRUCT_DESERIALIZATION + +// SDBUSCPP_REGISTER_STRUCT still references throw +#define SDBUSCPP_REGISTER_STRUCT_NO_THROW(STRUCT, ...) \ + namespace sdbus { \ + static_assert(SDBUSCPP_PP_NARG(__VA_ARGS__) <= 16, \ + "Not more than 16 struct members are supported, please open " \ + "an issue if you need more"); \ + \ + template <> \ + struct signature_of<STRUCT> \ + : signature_of<sdbus::Struct<SDBUSCPP_STRUCT_MEMBER_TYPES( \ + STRUCT, __VA_ARGS__)>> {}; \ + \ + inline auto as_dictionary_if_struct(const STRUCT& object) { \ + return as_dictionary<STRUCT>(object); \ + } \ + \ + inline sdbus::Message& operator<<(sdbus::Message& msg, \ + const STRUCT& items) { \ + return msg << sdbus::Struct{std::forward_as_tuple( \ + SDBUSCPP_STRUCT_MEMBERS(items, __VA_ARGS__))}; \ + } \ + \ + inline Message& operator<<(Message& msg, const as_dictionary<STRUCT>& s) { \ + if constexpr (!nested_struct_as_dict_serialization_v<STRUCT>) \ + return msg.serializeDictionary<std::string, Variant>( \ + {SDBUSCPP_STRUCT_MEMBERS_AS_DICT_ENTRIES(s.m_struct, __VA_ARGS__)}); \ + else \ + return msg.serializeDictionary<std::string, Variant>( \ + {SDBUSCPP_STRUCT_MEMBERS_AS_NESTED_DICT_ENTRIES(s.m_struct, \ + __VA_ARGS__)}); \ + } \ + \ + inline Message& operator>>(Message& msg, STRUCT& s) { \ + /* First, try to deserialize as a struct */ \ + if (msg.peekType().first == signature_of<STRUCT>::type_value) { \ + Struct sdbusStruct{ \ + std::forward_as_tuple(SDBUSCPP_STRUCT_MEMBERS(s, __VA_ARGS__))}; \ + return msg >> sdbusStruct; \ + } \ + \ + /* Otherwise try to deserialize as a dictionary of strings to variants */ \ + \ + return msg.deserializeDictionary<std::string, Variant>( \ + [&s](const auto& dictEntry) { \ + const std::string& key = \ + dictEntry \ + .first; /* Intentionally not using structured bindings */ \ + const Variant& value = dictEntry.second; \ + \ + using namespace std::string_literals; \ + /* This also handles members which are structs serialized as dict of strings to variants, recursively */ \ + SDBUSCPP_FIND_AND_DESERIALIZE_STRUCT_MEMBERS(s, __VA_ARGS__); \ + }); \ + } \ + } + +#endif // SDBUSCPP_NO_THROW_HELPER_HH |
