diff --git a/common/error_code.hpp b/common/error_code.hpp new file mode 100644 index 00000000..946eda69 --- /dev/null +++ b/common/error_code.hpp @@ -0,0 +1,61 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2021 Johannes Pohl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#ifndef ERROR_CODE_HPP +#define ERROR_CODE_HPP + + +#include +#include +#include + + +namespace snapcast +{ + +struct ErrorCode : public std::error_code +{ + ErrorCode() : std::error_code(), detail_(std::nullopt) + { + } + + ErrorCode(const std::error_code& code) : std::error_code(code), detail_(std::nullopt) + { + } + + ErrorCode(const std::error_code& code, std::string detail) : std::error_code(code), detail_(std::move(detail)) + { + } + + std::string detailed_message() const + { + if (detail_.has_value()) + return message() + ": " + *detail_; + return message(); + } + +private: + std::optional detail_; +}; + + +} // namespace snapcast + + + +#endif diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 88357b3a..e0ea658d 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -14,6 +14,7 @@ set(SERVER_SOURCES encoder/pcm_encoder.cpp encoder/null_encoder.cpp streamreader/base64.cpp + streamreader/control_error.cpp streamreader/stream_control.cpp streamreader/stream_uri.cpp streamreader/stream_manager.cpp diff --git a/server/Makefile b/server/Makefile index 388e9d28..24e719fc 100644 --- a/server/Makefile +++ b/server/Makefile @@ -44,7 +44,7 @@ endif CXXFLAGS += $(ADD_CFLAGS) -std=c++17 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_VORBIS -DHAS_VORBIS_ENC -DHAS_OPUS -DHAS_SOXR -DVERSION=\"$(VERSION)\" -I. -I.. -I../common LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus -lsoxr -OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/meta_stream.o streamreader/librespot_stream.o streamreader/watchdog.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/null_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o +OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/meta_stream.o streamreader/librespot_stream.o streamreader/watchdog.o streamreader/control_error.o streamreader/stream_control.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/null_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o ifneq (,$(TARGET)) CXXFLAGS += -D$(TARGET) diff --git a/server/server.cpp b/server/server.cpp index eeaf7d2a..c84055f3 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -459,12 +459,64 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon if (stream == nullptr) throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); - stream->control(*request, [request, on_response](const jsonrpcpp::Response& ctrl_response) { - LOG(INFO, LOG_TAG) << "Received response for Stream.Control, id: " << ctrl_response.id() << ", result: " << ctrl_response.result() - << ", error: " << ctrl_response.error().code() << "\n"; - auto response = make_shared(request->id(), ctrl_response.result()); + if (!request->params().has("command")) + throw jsonrpcpp::InvalidParamsException("Parameter 'commmand' is missing", request->id()); + + auto command = request->params().get("command"); + + auto handle_response = [request, on_response, command](const snapcast::ErrorCode& ec) { + LOG(DEBUG, LOG_TAG) << "Response to '" << command << "': " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message() + << ", category: " << ec.category().name() << "\n"; + std::shared_ptr response; + if (ec) + response = make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); + else + response = make_shared(request->id(), "ok"); on_response(response, nullptr); - }); + }; + + if (command == "SetPosition") + { + if (!request->params().has("params") || !request->params().get("params").contains("Position")) + throw jsonrpcpp::InvalidParamsException("SetPosition requires parameters 'Position'"); + auto seconds = request->params().get("params")["Position"].get(); + stream->setPosition(std::chrono::milliseconds(static_cast(seconds * 1000)), + [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "Seek") + { + if (!request->params().has("params") || !request->params().get("params").contains("Offset")) + throw jsonrpcpp::InvalidParamsException("Seek requires parameter 'Offset'"); + auto offset = request->params().get("params")["Offset"].get(); + stream->seek(std::chrono::milliseconds(static_cast(offset * 1000)), + [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "Next") + { + stream->next([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "Previous") + { + stream->previous([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "Pause") + { + stream->pause([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "PlayPause") + { + stream->playPause([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "Stop") + { + stream->stop([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "Play") + { + stream->play([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else + throw jsonrpcpp::InvalidParamsException("Command '" + command + "' not supported", request->id()); return; } @@ -482,12 +534,55 @@ void Server::processRequest(const jsonrpcpp::request_ptr request, const OnRespon if (stream == nullptr) throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); - stream->setProperty(*request, [request, on_response](const jsonrpcpp::Response& props_response) { - LOG(INFO, LOG_TAG) << "Received response for Stream.SetProperty, id: " << props_response.id() << ", result: " << props_response.result() - << ", error: " << props_response.error().code() << "\n"; - auto response = make_shared(request->id(), props_response.result()); + if (!request->params().has("property")) + throw jsonrpcpp::InvalidParamsException("Parameter 'property' is missing", request->id()); + + if (!request->params().has("value")) + throw jsonrpcpp::InvalidParamsException("Parameter 'value' is missing", request->id()); + + auto name = request->params().get("property"); + auto value = request->params().get("value"); + LOG(INFO, LOG_TAG) << "Stream '" << streamId << "' set property: " << name << " = " << value << "\n"; + + auto handle_response = [request, on_response](const snapcast::ErrorCode& ec) { + LOG(ERROR, LOG_TAG) << "SetShuffle: " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message() + << ", category: " << ec.category().name() << "\n"; + std::shared_ptr response; + if (ec) + response = make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); + else + response = make_shared(request->id(), "ok"); on_response(response, nullptr); - }); + }; + + if (name == "loopStatus") + { + auto val = value.get(); + LoopStatus loop_status = loop_status_from_string(val); + if (loop_status == LoopStatus::kUnknown) + throw jsonrpcpp::InvalidParamsException("Value for loopStatus must be one of 'none', 'track', 'playlist'", request->id()); + stream->setLoopStatus(loop_status, [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (name == "shuffle") + { + if (!value.is_boolean()) + throw jsonrpcpp::InvalidParamsException("Value for shuffle must be bool", request->id()); + stream->setShuffle(value.get(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (name == "volume") + { + if (!value.is_number_integer()) + throw jsonrpcpp::InvalidParamsException("Value for volume must be an int", request->id()); + stream->setVolume(value.get(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (name == "rate") + { + if (!value.is_number_float()) + throw jsonrpcpp::InvalidParamsException("Value for rate must be float", request->id()); + stream->setRate(value.get(), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else + throw jsonrpcpp::InvalidParamsException("Property '" + name + "' not supported", request->id()); return; } diff --git a/server/streamreader/control_error.cpp b/server/streamreader/control_error.cpp new file mode 100644 index 00000000..ea03c6d1 --- /dev/null +++ b/server/streamreader/control_error.cpp @@ -0,0 +1,89 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2021 Johannes Pohl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#include "control_error.hpp" + +namespace snapcast::error::control +{ + +namespace detail +{ + +struct category : public std::error_category +{ +public: + const char* name() const noexcept override; + std::string message(int value) const override; +}; + + +const char* category::name() const noexcept +{ + return "control"; +} + +std::string category::message(int value) const +{ + switch (static_cast(value)) + { + case ControlErrc::success: + return "Success"; + case ControlErrc::can_not_control: + return "Stream can not be controlled"; + case ControlErrc::can_go_previous_is_false: + return "Stream property can_go_previous is false"; + case ControlErrc::can_play_is_false: + return "Stream property can_play is false"; + case ControlErrc::can_pause_is_false: + return "Stream property can_pause is false"; + case ControlErrc::can_seek_is_false: + return "Stream property can_seek is false"; + case ControlErrc::can_control_is_false: + return "Stream property can_control is false"; + case ControlErrc::parse_error: + return "Parse error"; + case ControlErrc::invalid_request: + return "Invalid request"; + case ControlErrc::method_not_found: + return "Method not found"; + case ControlErrc::invalid_params: + return "Invalid params"; + case ControlErrc::internal_error: + return "Internal error"; + default: + return "Unknown"; + } +} + +} // namespace detail + +const std::error_category& category() +{ + // The category singleton + static detail::category instance; + return instance; +} + +} // namespace snapcast::error::control + +std::error_code make_error_code(ControlErrc errc) +{ + // Create an error_code with the original mpg123 error value + // and the mpg123 error category. + return std::error_code(static_cast(errc), snapcast::error::control::category()); +} diff --git a/server/streamreader/control_error.hpp b/server/streamreader/control_error.hpp new file mode 100644 index 00000000..feff4387 --- /dev/null +++ b/server/streamreader/control_error.hpp @@ -0,0 +1,82 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2021 Johannes Pohl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#ifndef STREAMREADER_ERROR_HPP +#define STREAMREADER_ERROR_HPP + + +#include + + +// https://www.boost.org/doc/libs/develop/libs/outcome/doc/html/motivation/plug_error_code.html +// https://akrzemi1.wordpress.com/examples/error_code-example/ +// https://breese.github.io/2017/05/12/customizing-error-codes.html +// http://blog.think-async.com/2010/04/system-error-support-in-c0x-part-5.html + + +enum class ControlErrc +{ + success = 0, + + // Stream can not be controlled + can_not_control = 1, + + // Stream property can_go_next is false + can_go_next_is_false = 2, + // Stream property can_go_previous is false + can_go_previous_is_false = 3, + // Stream property can_play is false + can_play_is_false = 4, + // Stream property can_pause is false + can_pause_is_false = 5, + // Stream property can_seek is false + can_seek_is_false = 6, + // Stream property can_control is false + can_control_is_false = 7, + + // Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. + parse_error = -32700, + // The JSON sent is not a valid Request object. + invalid_request = -32600, + // The method does not exist / is not available. + method_not_found = -32601, + // Invalid method parameter(s). + invalid_params = -32602, + // Internal JSON-RPC error. + internal_error = -32603 +}; + +namespace snapcast::error::control +{ +const std::error_category& category(); +} + + + +namespace std +{ +template <> +struct is_error_code_enum : public std::true_type +{ +}; +} // namespace std + +std::error_code make_error_code(ControlErrc); + + +#endif diff --git a/server/streamreader/meta_stream.cpp b/server/streamreader/meta_stream.cpp index cfc2fdd1..d931d880 100644 --- a/server/streamreader/meta_stream.cpp +++ b/server/streamreader/meta_stream.cpp @@ -87,7 +87,7 @@ void MetaStream::stop() void MetaStream::onMetadataChanged(const PcmStream* pcmStream, const Metatags& metadata) { LOG(DEBUG, LOG_TAG) << "onMetadataChanged: " << pcmStream->getName() << "\n"; - std::lock_guard lock(mutex_); + std::lock_guard lock(mutex_); if (pcmStream != active_stream_.get()) return; setMetadata(metadata); @@ -97,7 +97,7 @@ void MetaStream::onMetadataChanged(const PcmStream* pcmStream, const Metatags& m void MetaStream::onPropertiesChanged(const PcmStream* pcmStream, const Properties& properties) { LOG(DEBUG, LOG_TAG) << "onPropertiesChanged: " << pcmStream->getName() << "\n"; - std::lock_guard lock(mutex_); + std::lock_guard lock(mutex_); if (pcmStream != active_stream_.get()) return; setProperties(properties); @@ -107,7 +107,22 @@ void MetaStream::onPropertiesChanged(const PcmStream* pcmStream, const Propertie void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state) { LOG(DEBUG, LOG_TAG) << "onStateChanged: " << pcmStream->getName() << ", state: " << state << "\n"; - std::lock_guard lock(mutex_); + std::lock_guard lock(mutex_); + + if (active_stream_->getProperties().playback_status == PlaybackStatus::kPaused) + return; + + auto switch_stream = [this](std::shared_ptr new_stream) { + if (new_stream == active_stream_) + return; + LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_ ? active_stream_->getName() : "") << " => " + << new_stream->getName() << "\n"; + active_stream_ = new_stream; + setMetadata(active_stream_->getMetadata()); + setProperties(active_stream_->getProperties()); + resampler_ = make_unique(active_stream_->getSampleFormat(), sampleFormat_); + }; + for (const auto& stream : streams_) { if (stream->getState() == ReaderState::kPlaying) @@ -117,19 +132,15 @@ void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state) if (active_stream_ != stream) { - LOG(INFO, LOG_TAG) << "Stream: " << name_ << ", switching active stream: " << (active_stream_ ? active_stream_->getName() : "") << " => " - << stream->getName() << "\n"; - active_stream_ = stream; - setMetadata(active_stream_->getMetadata()); - setProperties(active_stream_->getProperties()); - resampler_ = make_unique(active_stream_->getSampleFormat(), sampleFormat_); + switch_stream(stream); } setState(ReaderState::kPlaying); return; } } - active_stream_ = streams_.front(); + + switch_stream(streams_.front()); setState(ReaderState::kIdle); } @@ -137,7 +148,7 @@ void MetaStream::onStateChanged(const PcmStream* pcmStream, ReaderState state) void MetaStream::onChunkRead(const PcmStream* pcmStream, const msg::PcmChunk& chunk) { // LOG(TRACE, LOG_TAG) << "onChunkRead: " << pcmStream->getName() << ", duration: " << chunk.durationMs() << "\n"; - std::lock_guard lock(mutex_); + std::lock_guard lock(mutex_); if (pcmStream != active_stream_.get()) return; // active_stream_->sampleFormat_ @@ -195,24 +206,104 @@ void MetaStream::onChunkEncoded(const PcmStream* pcmStream, std::shared_ptrgetName() << ", duration: " << ms << " ms\n"; - std::lock_guard lock(mutex_); + std::lock_guard lock(mutex_); if (pcmStream != active_stream_.get()) return; resync(std::chrono::nanoseconds(static_cast(ms * 1000000))); } -void MetaStream::setProperty(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler) + +// Setter for properties +void MetaStream::setShuffle(bool shuffle, ResultHandler handler) { - std::lock_guard lock(mutex_); - active_stream_->setProperty(request, response_handler); + std::lock_guard lock(mutex_); + active_stream_->setShuffle(shuffle, std::move(handler)); +} + +void MetaStream::setLoopStatus(LoopStatus status, ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->setLoopStatus(status, std::move(handler)); +} + +void MetaStream::setVolume(uint16_t volume, ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->setVolume(volume, std::move(handler)); +} + +void MetaStream::setRate(float rate, ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->setRate(rate, std::move(handler)); } -void MetaStream::control(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler) +// Control commands +void MetaStream::setPosition(std::chrono::milliseconds position, ResultHandler handler) { - std::lock_guard lock(mutex_); - active_stream_->control(request, response_handler); + std::lock_guard lock(mutex_); + active_stream_->setPosition(position, std::move(handler)); +} + +void MetaStream::seek(std::chrono::milliseconds offset, ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->seek(offset, std::move(handler)); +} + +void MetaStream::next(ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->next(std::move(handler)); +} + +void MetaStream::previous(ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->previous(std::move(handler)); +} + +void MetaStream::pause(ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->pause(std::move(handler)); +} + +void MetaStream::playPause(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "PlayPause\n"; + std::lock_guard lock(mutex_); + if (active_stream_->getState() == ReaderState::kIdle) + play(handler); + else + active_stream_->playPause(std::move(handler)); +} + +void MetaStream::stop(ResultHandler handler) +{ + std::lock_guard lock(mutex_); + active_stream_->stop(std::move(handler)); +} + +void MetaStream::play(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "Play\n"; + std::lock_guard lock(mutex_); + if ((active_stream_->getProperties().can_play) && (active_stream_->getProperties().playback_status != PlaybackStatus::kPlaying)) + return active_stream_->play(std::move(handler)); + + for (const auto& stream : streams_) + { + if ((stream->getState() == ReaderState::kIdle) && (stream->getProperties().can_play)) + { + return stream->play(std::move(handler)); + } + } + + // call play on the active stream to get the handler called + active_stream_->play(std::move(handler)); } diff --git a/server/streamreader/meta_stream.hpp b/server/streamreader/meta_stream.hpp index f2537af6..b651c129 100644 --- a/server/streamreader/meta_stream.hpp +++ b/server/streamreader/meta_stream.hpp @@ -44,6 +44,22 @@ public: void start() override; void stop() override; + // Setter for properties + void setShuffle(bool shuffle, ResultHandler handler) override; + void setLoopStatus(LoopStatus status, ResultHandler handler) override; + void setVolume(uint16_t volume, ResultHandler handler) override; + void setRate(float rate, ResultHandler handler) override; + + // Control commands + void setPosition(std::chrono::milliseconds position, ResultHandler handler) override; + void seek(std::chrono::milliseconds offset, ResultHandler handler) override; + void next(ResultHandler handler) override; + void previous(ResultHandler handler) override; + void pause(ResultHandler handler) override; + void playPause(ResultHandler handler) override; + void stop(ResultHandler handler) override; + void play(ResultHandler handler) override; + protected: /// Implementation of PcmListener void onMetadataChanged(const PcmStream* pcmStream, const Metatags& metadata) override; @@ -54,12 +70,9 @@ protected: void onResync(const PcmStream* pcmStream, double ms) override; protected: - void setProperty(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler) override; - void control(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler) override; - std::vector> streams_; std::shared_ptr active_stream_; - std::mutex mutex_; + std::recursive_mutex mutex_; std::unique_ptr resampler_; bool first_read_; std::chrono::time_point next_tick_; diff --git a/server/streamreader/pcm_stream.cpp b/server/streamreader/pcm_stream.cpp index 7e113e4f..58493622 100644 --- a/server/streamreader/pcm_stream.cpp +++ b/server/streamreader/pcm_stream.cpp @@ -21,9 +21,11 @@ #include #include "common/aixlog.hpp" +#include "common/error_code.hpp" #include "common/snap_exception.hpp" #include "common/str_compat.hpp" #include "common/utils/string_utils.hpp" +#include "control_error.hpp" #include "encoder/encoder_factory.hpp" #include "pcm_stream.hpp" @@ -326,126 +328,127 @@ const Properties& PcmStream::getProperties() const } -void PcmStream::setProperty(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler) +void PcmStream::setShuffle(bool shuffle, ResultHandler handler) { - try - { - if (!request.params().has("property")) - throw SnapException("Parameter 'property' is missing"); - - if (!request.params().has("value")) - throw SnapException("Parameter 'value' is missing"); - - auto name = request.params().get("property"); - auto value = request.params().get("value"); - LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' set property: " << name << " = " << value << "\n"; - - if (name == "loopStatus") - { - auto val = value.get(); - if ((val != "none") || (val != "track") || (val != "playlist")) - throw SnapException("Value for loopStatus must be one of 'none', 'track', 'playlist'"); - } - else if (name == "shuffle") - { - if (!value.is_boolean()) - throw SnapException("Value for shuffle must be bool"); - } - else if (name == "volume") - { - if (!value.is_number_integer()) - throw SnapException("Value for volume must be an int"); - } - else if (name == "rate") - { - if (!value.is_number_float()) - throw SnapException("Value for rate must be float"); - } - - if (!properties_.can_control) - throw SnapException("CanControl is false"); - - if (stream_ctrl_) - { - jsonrpcpp::Request req(++req_id_, "Plugin.Stream.Player.SetProperty", {name, value}); - stream_ctrl_->command(req, response_handler); - } - } - catch (const std::exception& e) - { - LOG(WARNING, LOG_TAG) << "Error in setProperty: " << e.what() << '\n'; - auto error = jsonrpcpp::InvalidParamsException(e.what(), request.id()); - response_handler(error.to_json()); - } + LOG(DEBUG, LOG_TAG) << "setShuffle: " << shuffle << "\n"; + sendRequest("Plugin.Stream.Player.SetProperty", {"shuffle", shuffle}, std::move(handler)); } -void PcmStream::control(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler) +void PcmStream::setLoopStatus(LoopStatus status, ResultHandler handler) { - try - { - if (!request.params().has("command")) - throw SnapException("Parameter 'command' is missing"); + LOG(DEBUG, LOG_TAG) << "setLoopStatus: " << status << "\n"; + sendRequest("Plugin.Stream.Player.SetProperty", {"loopStatus", to_string(status)}, std::move(handler)); +} - std::string command = request.params().get("command"); - if (command == "SetPosition") - { - if (!request.params().has("params") || !request.params().get("params").contains("Position")) - throw SnapException("SetPosition requires parameters 'Position' and optionally 'TrackId'"); - if (!properties_.can_seek) - throw SnapException("CanSeek is false"); - } - else if (command == "Seek") - { - if (!request.params().has("params") || !request.params().get("params").contains("Offset")) - throw SnapException("Seek requires parameter 'Offset'"); - if (!properties_.can_seek) - throw SnapException("CanSeek is false"); - } - else if (command == "Next") - { - if (!properties_.can_go_next) - throw SnapException("CanGoNext is false"); - } - else if (command == "Previous") - { - if (!properties_.can_go_previous) - throw SnapException("CanGoPrevious is false"); - } - else if ((command == "Pause") || (command == "PlayPause")) - { - if (!properties_.can_pause) - throw SnapException("CanPause is false"); - } - else if (command == "Stop") - { - if (!properties_.can_control) - throw SnapException("CanControl is false"); - } - else if (command == "Play") - { - if (!properties_.can_play) - throw SnapException("CanPlay is false"); - } + +void PcmStream::setVolume(uint16_t volume, ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "setVolume: " << volume << "\n"; + sendRequest("Plugin.Stream.Player.SetProperty", {"volume", volume}, std::move(handler)); +} + + +void PcmStream::setRate(float rate, ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "setRate: " << rate << "\n"; + sendRequest("Plugin.Stream.Player.SetProperty", {"rate", rate}, std::move(handler)); +} + + +void PcmStream::setPosition(std::chrono::milliseconds position, ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "setPosition\n"; + if (!properties_.can_seek) + return handler({ControlErrc::can_seek_is_false}); + json params; + params["command"] = "SetPosition"; + json j; + j["Position"] = position.count() / 1000.f; + params["params"] = j; + sendRequest("Plugin.Stream.Player.Control", params, std::move(handler)); +} + + +void PcmStream::seek(std::chrono::milliseconds offset, ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "seek\n"; + if (!properties_.can_seek) + return handler({ControlErrc::can_seek_is_false}); + json params; + params["command"] = "Seek"; + json j; + j["Offset"] = offset.count() / 1000.f; + params["params"] = j; + sendRequest("Plugin.Stream.Player.Control", params, std::move(handler)); +} + + +void PcmStream::next(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "next\n"; + if (!properties_.can_go_next) + return handler({ControlErrc::can_go_next_is_false}); + sendRequest("Plugin.Stream.Player.Control", {"command", "Next"}, std::move(handler)); +} + + +void PcmStream::previous(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "previous\n"; + if (!properties_.can_go_previous) + return handler({ControlErrc::can_go_previous_is_false}); + sendRequest("Plugin.Stream.Player.Control", {"command", "Previous"}, std::move(handler)); +} + + +void PcmStream::pause(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "pause\n"; + if (!properties_.can_pause) + return handler({ControlErrc::can_pause_is_false}); + sendRequest("Plugin.Stream.Player.Control", {"command", "Pause"}, std::move(handler)); +} + +void PcmStream::sendRequest(const std::string& method, const jsonrpcpp::Parameter& params, ResultHandler handler) +{ + if (!stream_ctrl_) + return handler({ControlErrc::can_not_control}); + + jsonrpcpp::Request req(++req_id_, method, params); + stream_ctrl_->command(req, [handler](const jsonrpcpp::Response& response) { + if (response.error().code()) + handler({static_cast(response.error().code()), response.error().data()}); else - throw SnapException("Command not supported"); + handler({ControlErrc::success}); + }); +} - LOG(INFO, LOG_TAG) << "Stream '" << getId() << "' received command: '" << command << "', params: '" << request.params().to_json() << "'\n"; - if (stream_ctrl_) - { - jsonrpcpp::Parameter params{"command", command}; - if (request.params().has("params")) - params.add("params", request.params().get("params")); - jsonrpcpp::Request req(++req_id_, "Plugin.Stream.Player.Control", params); - stream_ctrl_->command(req, response_handler); - } - } - catch (const std::exception& e) - { - LOG(WARNING, LOG_TAG) << "Error in control: " << e.what() << '\n'; - auto error = jsonrpcpp::InvalidParamsException(e.what(), request.id()); - response_handler(error.to_json()); - } + +void PcmStream::playPause(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "playPause\n"; + if (!properties_.can_pause) + return handler({ControlErrc::can_play_is_false}); + sendRequest("Plugin.Stream.Player.Control", {"command", "PlayPause"}, std::move(handler)); +} + + +void PcmStream::stop(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "stop\n"; + if (!properties_.can_control) + return handler({ControlErrc::can_control_is_false}); + sendRequest("Plugin.Stream.Player.Control", {"command", "Stop"}, std::move(handler)); +} + + +void PcmStream::play(ResultHandler handler) +{ + LOG(DEBUG, LOG_TAG) << "play\n"; + if (!properties_.can_play) + return handler({ControlErrc::can_play_is_false}); + sendRequest("Plugin.Stream.Player.Control", {"command", "Play"}, std::move(handler)); } diff --git a/server/streamreader/pcm_stream.hpp b/server/streamreader/pcm_stream.hpp index 07e1b6aa..854f4b9b 100644 --- a/server/streamreader/pcm_stream.hpp +++ b/server/streamreader/pcm_stream.hpp @@ -28,6 +28,7 @@ #include #include +#include "common/error_code.hpp" #include "common/json.hpp" #include "common/metatags.hpp" #include "common/properties.hpp" @@ -116,6 +117,8 @@ public: class PcmStream { public: + using ResultHandler = std::function; + /// ctor. Encoded PCM data is passed to the PcmListener PcmStream(PcmListener* pcmListener, boost::asio::io_context& ioc, const ServerSettings& server_settings, const StreamUri& uri); virtual ~PcmStream(); @@ -134,8 +137,21 @@ public: const Metatags& getMetadata() const; const Properties& getProperties() const; - virtual void setProperty(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler); - virtual void control(const jsonrpcpp::Request& request, const StreamControl::OnResponse& response_handler); + // Setter for properties + virtual void setShuffle(bool shuffle, ResultHandler handler); + virtual void setLoopStatus(LoopStatus status, ResultHandler handler); + virtual void setVolume(uint16_t volume, ResultHandler handler); + virtual void setRate(float rate, ResultHandler handler); + + // Control commands + virtual void setPosition(std::chrono::milliseconds position, ResultHandler handler); + virtual void seek(std::chrono::milliseconds offset, ResultHandler handler); + virtual void next(ResultHandler handler); + virtual void previous(ResultHandler handler); + virtual void pause(ResultHandler handler); + virtual void playPause(ResultHandler handler); + virtual void stop(ResultHandler handler); + virtual void play(ResultHandler handler); virtual ReaderState getState() const; virtual json toJson() const; @@ -145,10 +161,6 @@ public: protected: std::atomic active_; - void onControlRequest(const jsonrpcpp::Request& request); - void onControlNotification(const jsonrpcpp::Notification& notification); - void onControlLog(std::string line); - void setState(ReaderState newState); void chunkRead(const msg::PcmChunk& chunk); void resync(const std::chrono::nanoseconds& duration); @@ -159,6 +171,16 @@ protected: void pollProperties(); + // script callbacks + /// Request received from control script + void onControlRequest(const jsonrpcpp::Request& request); + /// Notification received from control script + void onControlNotification(const jsonrpcpp::Notification& notification); + /// Log message received from control script via stderr + void onControlLog(std::string line); + /// Send request to stream control script + void sendRequest(const std::string& method, const jsonrpcpp::Parameter& params, ResultHandler handler); + std::chrono::time_point tvEncodedChunk_; std::vector pcmListeners_; StreamUri uri_; @@ -177,6 +199,7 @@ protected: mutable std::recursive_mutex mutex_; }; + } // namespace streamreader #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7d16ffa4..d4f38561 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -9,7 +9,11 @@ if (ANDROID) endif (ANDROID) # Make test executable -set(TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp ${CMAKE_SOURCE_DIR}/server/streamreader/stream_uri.cpp) +set(TEST_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp + ${CMAKE_SOURCE_DIR}/server/streamreader/control_error.cpp + ${CMAKE_SOURCE_DIR}/server/streamreader/stream_uri.cpp) + add_executable(snapcast_test ${TEST_SOURCES}) target_link_libraries(snapcast_test ${TEST_LIBRARIES}) diff --git a/test/test_main.cpp b/test/test_main.cpp index d2d547df..0b49c088 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -25,6 +25,7 @@ #include "common/metatags.hpp" #include "common/properties.hpp" #include "common/utils/string_utils.hpp" +#include "server/streamreader/control_error.hpp" #include "server/streamreader/stream_uri.hpp" using namespace std; @@ -237,3 +238,17 @@ TEST_CASE("Librespot") for (const auto& match : m) std::cerr << "Match: '" << match << "'\n"; } + + +TEST_CASE("Error") +{ + std::error_code ec = ControlErrc::can_not_control; + REQUIRE(ec); + REQUIRE(ec == ControlErrc::can_not_control); + REQUIRE(ec != ControlErrc::success); + std::cout << ec << std::endl; + + ec = make_error_code(ControlErrc::can_not_control); + REQUIRE(ec.category() == snapcast::error::control::category()); + std::cout << "Category: " << ec.category().name() << ", " << ec.message() << std::endl; +}