diff --git a/common/properties.hpp b/common/properties.hpp index a08dfec5..28ae6732 100644 --- a/common/properties.hpp +++ b/common/properties.hpp @@ -19,6 +19,7 @@ #ifndef PROPERTIES_HPP #define PROPERTIES_HPP +#include #include #include #include @@ -95,8 +96,6 @@ static std::ostream& operator<<(std::ostream& os, LoopStatus loop_status) class Properties { - static constexpr auto LOG_TAG = "Properties"; - public: Properties() = default; Properties(const json& j) @@ -112,12 +111,16 @@ public: /// A value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some /// other order. boost::optional shuffle; + /// The current playback rate + boost::optional rate; /// The volume level between 0-100 boost::optional volume; /// The current track position in seconds boost::optional position; - // /// The current track duration in seconds - // boost::optional duration; + /// The minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value + boost::optional minimum_rate; + /// The maximum value which the Rate property can take. Clients should not attempt to set the Rate property above this value + boost::optional maximum_rate; /// Whether the client can call the Next method on this interface and expect the current track to change boost::optional can_go_next; /// Whether the client can call the Previous method on this interface and expect the current track to change @@ -134,14 +137,17 @@ public: json toJson() const { json j; - if (playback_status.has_value()) - addTag(j, "playbackStatus", boost::optional(to_string(playback_status.value()))); if (loop_status.has_value()) addTag(j, "loopStatus", boost::optional(to_string(loop_status.value()))); addTag(j, "shuffle", shuffle); addTag(j, "volume", volume); + addTag(j, "rate", rate); + + if (playback_status.has_value()) + addTag(j, "playbackStatus", boost::optional(to_string(playback_status.value()))); addTag(j, "position", position); - // addTag(j, "duration", duration); + addTag(j, "minimumRate", minimum_rate); + addTag(j, "maximumRate", maximum_rate); addTag(j, "canGoNext", can_go_next); addTag(j, "canGoPrevious", can_go_previous); addTag(j, "canPlay", can_play); @@ -153,29 +159,19 @@ public: void fromJson(const json& j) { - static std::set supported_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "position", "canGoNext", - "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl"}; + static std::set rw_props = {"loopStatus", "shuffle", "volume", "rate"}; + static std::set ro_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "position", "minimumRate", "maximumRate", + "canGoNext", "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl"}; for (const auto& element : j.items()) { - if (supported_props.find(element.key()) == supported_props.end()) + bool is_rw = (rw_props.find(element.key()) != rw_props.end()); + bool is_ro = (ro_props.find(element.key()) != ro_props.end()); + if (!is_rw && !is_ro) LOG(WARNING, LOG_TAG) << "Property not supoorted: " << element.key() << "\n"; } boost::optional opt; - readTag(j, "playbackStatus", opt); - if (opt.has_value()) - { - if (*opt == "playing") - playback_status = PlaybackStatus::kPlaying; - else if (*opt == "paused") - playback_status = PlaybackStatus::kPaused; - else if (*opt == "stopped") - playback_status = PlaybackStatus::kStopped; - else - playback_status = PlaybackStatus::kUnknown; - } - else - playback_status = boost::none; + readTag(j, "loopStatus", opt); if (opt.has_value()) { @@ -192,8 +188,25 @@ public: loop_status = boost::none; readTag(j, "shuffle", shuffle); readTag(j, "volume", volume); + readTag(j, "rate", rate); + + readTag(j, "playbackStatus", opt); + if (opt.has_value()) + { + if (*opt == "playing") + playback_status = PlaybackStatus::kPlaying; + else if (*opt == "paused") + playback_status = PlaybackStatus::kPaused; + else if (*opt == "stopped") + playback_status = PlaybackStatus::kStopped; + else + playback_status = PlaybackStatus::kUnknown; + } + else + playback_status = boost::none; readTag(j, "position", position); - // readTag(j, "duration", duration); + readTag(j, "minimumRate", minimum_rate); + readTag(j, "maximumRate", maximum_rate); readTag(j, "canGoNext", can_go_next); readTag(j, "canGoPrevious", can_go_previous); readTag(j, "canPlay", can_play); @@ -237,6 +250,9 @@ private: LOG(ERROR, LOG_TAG) << "failed to add tag: '" << tag << "': " << e.what() << '\n'; } } + +private: + static constexpr auto LOG_TAG = "Properties"; }; diff --git a/control/meta_mpd.py b/control/meta_mpd.py index 419731c6..26cf9bec 100755 --- a/control/meta_mpd.py +++ b/control/meta_mpd.py @@ -395,13 +395,13 @@ class MPDWrapper(object): if offset >= 0: strOffset = "+" + strOffset self.seekcur(strOffset) - elif cmd == 'SetProperties': - properties = request['params'] - logger.info(f'SetProperties: {properties}') - if 'shuffle' in properties: - self.random(int(properties['shuffle'])) - if 'loopStatus' in properties: - value = properties['loopStatus'] + elif cmd == 'SetProperty': + property = request['params'] + logger.info(f'SetProperty: {property}') + if 'shuffle' in property: + self.random(int(property['shuffle'])) + if 'loopStatus' in property: + value = property['loopStatus'] if value == "playlist": self.repeat(1) if self._can_single: @@ -438,7 +438,6 @@ class MPDWrapper(object): for char in chunk: if char == '\n': logger.info(f'Received: {self._buffer}') - self.control(self._buffer) self._buffer = '' else: @@ -628,7 +627,7 @@ class MPDWrapper(object): logger.debug( f'key: {key}, value: {value}, mapped key: {mapped_key}, mapped value: {mapped_val}') except KeyError: - logger.debug(f'tag "{key}" not supported') + logger.debug(f'property "{key}" not supported') except (ValueError, TypeError): logger.warning( f"Can't cast value {value} to {status_mapping[key][1]}") diff --git a/control/snapcast_mpris.py b/control/snapcast_mpris.py index 00d93c33..353569c4 100755 --- a/control/snapcast_mpris.py +++ b/control/snapcast_mpris.py @@ -21,6 +21,7 @@ from time import sleep +from systemd.journal import _valid_field_name import websocket import logging import threading @@ -467,11 +468,9 @@ class SnapcastWrapper(object): "id": self._stream_id, "command": command, "params": params}) def set_property(self, property, value): - properties = {} - properties[property] = value - logger.info(f'set_properties {properties}') - self.send_request("Stream.SetProperties", { - "id": self._stream_id, "properties": properties}) + logger.info(f'set_property {property} = {value}') + self.send_request("Stream.SetProperty", { + "id": self._stream_id, "property": property, "value": value}) @property def metadata(self): diff --git a/server/server.cpp b/server/server.cpp index ca0f9e2c..4d5447d4 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -463,13 +463,13 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::ent // Setup response result["id"] = streamId; } - else if (request->method().find("Stream.SetProperties") == 0) + else if (request->method().find("Stream.SetProperty") == 0) { // clang-format off // clang-format on - LOG(INFO, LOG_TAG) << "Stream.SetProperties id: " << request->params().get("id") - << ", properties: " << request->params().get("properties") << "\n"; + LOG(INFO, LOG_TAG) << "Stream.SetProperty id: " << request->params().get("id") + << ", property: " << request->params().get("property") << ", value: " << request->params().get("value") << "\n"; // Find stream string streamId = request->params().get("id"); @@ -477,12 +477,10 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, jsonrpcpp::ent if (stream == nullptr) throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); - Properties props(request->params().get("properties")); - - // Set metadata from request - stream->setProperties(props); + stream->setProperty(request->params().get("property"), request->params().get("value")); // Setup response + // TODO: error handling result["id"] = streamId; } else if (request->method() == "Stream.AddStream") diff --git a/server/streamreader/pcm_stream.cpp b/server/streamreader/pcm_stream.cpp index 1f9f2248..379c6033 100644 --- a/server/streamreader/pcm_stream.cpp +++ b/server/streamreader/pcm_stream.cpp @@ -262,13 +262,7 @@ void PcmStream::onControlMsg(const std::string& msg) else if (notification->method() == "Player.Properties") { LOG(DEBUG, LOG_TAG) << "Received properties notification\n"; - properties_ = std::make_shared(notification->params().to_json()); - // Trigger a stream update - for (auto* listener : pcmListeners_) - { - if (listener != nullptr) - listener->onPropertiesChanged(this); - } + setProperties(notification->params().to_json()); } else LOG(WARNING, LOG_TAG) << "Received unknown notification method: '" << notification->method() << "'\n"; @@ -405,24 +399,38 @@ std::shared_ptr PcmStream::getProperties() const } -void PcmStream::setProperties(const Properties& props) +void PcmStream::setProperty(const std::string& name, const json& value) { - LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' set properties: " << props.toJson() << "\n"; + LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' set property: " << name << " = " << value << "\n"; + // TODO: check validity + if (name == "loopStatus") + ; + else if (name == "shuffle") + ; + else if (name == "volume") + ; + else if (name == "rate") + ; + else + { + LOG(ERROR, LOG_TAG) << "Property not supported: " << name << "\n"; + } + // TODO: queue commands, send next on timeout or after reception of the last command's response if (ctrl_script_) { - jsonrpcpp::Request request(++req_id_, "Player.SetProperties", props.toJson()); + jsonrpcpp::Request request(++req_id_, "Player.SetProperty", {name, value}); ctrl_script_->send(request.to_json().dump() + "\n"); //, params); } else // TODO: Will the ctr_script always loop back the new properties? { - properties_ = std::make_shared(props); - // Trigger a stream update - for (auto* listener : pcmListeners_) - { - if (listener != nullptr) - listener->onPropertiesChanged(this); - } + // properties_ = std::make_shared(props); + // // Trigger a stream update + // for (auto* listener : pcmListeners_) + // { + // if (listener != nullptr) + // listener->onPropertiesChanged(this); + // } } } @@ -446,7 +454,7 @@ void PcmStream::control(const std::string& command, const json& params) void PcmStream::setMeta(const Metatags& meta) { meta_ = std::make_shared(meta); - LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", metadata=" << meta_->toJson().dump(4) << "\n"; + LOG(INFO, LOG_TAG) << "setMeta, stream: " << getId() << ", metadata: " << meta_->toJson() << "\n"; // Trigger a stream update for (auto* listener : pcmListeners_) @@ -456,4 +464,20 @@ void PcmStream::setMeta(const Metatags& meta) } } + +void PcmStream::setProperties(const Properties& props) +{ + properties_ = std::make_shared(props); + LOG(INFO, LOG_TAG) << "setProperties, stream: " << getId() << ", properties: " << props.toJson() << "\n"; + + // Trigger a stream update + for (auto* listener : pcmListeners_) + { + if (listener != nullptr) + listener->onPropertiesChanged(this); + } +} + + + } // namespace streamreader diff --git a/server/streamreader/pcm_stream.hpp b/server/streamreader/pcm_stream.hpp index cc452567..388be32e 100644 --- a/server/streamreader/pcm_stream.hpp +++ b/server/streamreader/pcm_stream.hpp @@ -172,7 +172,7 @@ public: std::shared_ptr getMeta() const; std::shared_ptr getProperties() const; - void setProperties(const Properties& props); + void setProperty(const std::string& name, const json& value); virtual void control(const std::string& command, const json& params); @@ -191,6 +191,7 @@ protected: void chunkEncoded(const encoder::Encoder& encoder, std::shared_ptr chunk, double duration); void setMeta(const Metatags& meta); + void setProperties(const Properties& props); std::chrono::time_point tvEncodedChunk_; std::vector pcmListeners_;