summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-10-20 21:29:01 +0200
committerJoel Klinghed <the_jk@spawned.biz>2025-10-20 21:29:01 +0200
commite8dc8edad7cdf194091f0479b70b154e872f57ef (patch)
tree5431680ce100812b9b3bea32a7847e7dcfdcf29d
parent0687ec31d1d75500beaee0e983ebf73d7c4517f7 (diff)
bt & main: Add optional player for device
-rw-r--r--src/bt.cc288
-rw-r--r--src/bt.hh47
-rw-r--r--src/main.cc54
-rw-r--r--src/sdbuscpp_no_throw_helper.hh61
4 files changed, 448 insertions, 2 deletions
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 <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_;
};
diff --git a/src/bt.hh b/src/bt.hh
index 588a328..bc7cd84 100644
--- a/src/bt.hh
+++ b/src/bt.hh
@@ -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