diff --git a/NEWS b/NEWS index 1dc977395..cccbc603a 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,8 @@ ver 0.23 (not yet released) * protocol - new command "getvol" +* output + - snapcast: new plugin ver 0.22.6 (2021/02/16) * fix missing tags on songs in queue diff --git a/doc/plugins.rst b/doc/plugins.rst index 454140500..093b157b4 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -1146,6 +1146,29 @@ audio API. Its primary use is local playback on Android, where floating point samples. +snapcast +-------- + +Snapcast is a multiroom client-server audio player. This plugin +allows MPD to acts as a `Snapcast +`__ server. Snapcast clients +connect to it and receive audio data from MPD. + +.. list-table:: + :widths: 20 80 + :header-rows: 1 + + * - Setting + - Description + * - **port P** + - Binds the Snapcast server to the specified port. The default + port is :samp:`1704`. + * - **bind_to_address ADDR** + - Binds the Snapcast server to the specified address. Multiple + addresses in parallel are not supported. The default is to + bind on all addresses on port :samp:`1704`. + + solaris ------- The "Solaris" plugin runs only on SUN Solaris, and plays via /dev/audio. diff --git a/meson_options.txt b/meson_options.txt index 36214692d..e327f6bdd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -177,6 +177,7 @@ option('pipe', type: 'boolean', value: true, description: 'Pipe output plugin') option('pulse', type: 'feature', description: 'PulseAudio support') option('recorder', type: 'boolean', value: true, description: 'Recorder output plugin') option('shout', type: 'feature', description: 'Shoutcast streaming support using libshout') +option('snapcast', type: 'boolean', value: true, description: 'Snapcast output plugin') option('sndio', type: 'feature', description: 'sndio output plugin') option('solaris_output', type: 'feature', description: 'Solaris /dev/audio support') diff --git a/src/output/Registry.cxx b/src/output/Registry.cxx index 21c117c9e..b5977c352 100644 --- a/src/output/Registry.cxx +++ b/src/output/Registry.cxx @@ -25,6 +25,7 @@ #include "plugins/AoOutputPlugin.hxx" #include "plugins/FifoOutputPlugin.hxx" #include "plugins/SndioOutputPlugin.hxx" +#include "plugins/snapcast/SnapcastOutputPlugin.hxx" #include "plugins/httpd/HttpdOutputPlugin.hxx" #include "plugins/HaikuOutputPlugin.hxx" #include "plugins/JackOutputPlugin.hxx" @@ -93,6 +94,9 @@ constexpr const AudioOutputPlugin *audio_output_plugins[] = { #ifdef ENABLE_HTTPD_OUTPUT &httpd_output_plugin, #endif +#ifdef ENABLE_SNAPCAST_OUTPUT + &snapcast_output_plugin, +#endif #ifdef ENABLE_RECORDER_OUTPUT &recorder_output_plugin, #endif diff --git a/src/output/plugins/meson.build b/src/output/plugins/meson.build index 8d48674ae..50618b866 100644 --- a/src/output/plugins/meson.build +++ b/src/output/plugins/meson.build @@ -114,6 +114,19 @@ if libsndio_dep.found() output_plugins_sources += 'SndioOutputPlugin.cxx' endif +output_features.set('ENABLE_SNAPCAST_OUTPUT', get_option('snapcast')) +if get_option('snapcast') + output_plugins_sources += [ + 'snapcast/SnapcastOutputPlugin.cxx', + 'snapcast/Client.cxx', + ] + output_plugins_deps += [ event_dep, net_dep ] + + # TODO: the Snapcast plugin needs just the "wave" encoder, but this + # enables all available encoders + need_encoder = true +endif + enable_solaris_output = get_option('solaris_output') if enable_solaris_output.auto() enable_solaris_output = host_machine.system() == 'sunos' or host_machine.system() == 'solaris' diff --git a/src/output/plugins/snapcast/Client.cxx b/src/output/plugins/snapcast/Client.cxx new file mode 100644 index 000000000..4e0711071 --- /dev/null +++ b/src/output/plugins/snapcast/Client.cxx @@ -0,0 +1,245 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "Client.hxx" +#include "Protocol.hxx" +#include "Timestamp.hxx" +#include "Internal.hxx" +#include "tag/RiffFormat.hxx" +#include "net/SocketError.hxx" +#include "net/UniqueSocketDescriptor.hxx" +#include "util/StringView.hxx" +#include "Log.hxx" + +#include +#include + +SnapcastClient::SnapcastClient(SnapcastOutput &_output, + UniqueSocketDescriptor _fd) noexcept + :BufferedSocket(_fd.Release(), _output.GetEventLoop()), + output(_output) +{ +} + +SnapcastClient::~SnapcastClient() noexcept +{ + if (IsDefined()) + BufferedSocket::Close(); +} + +void +SnapcastClient::Close() noexcept +{ + output.RemoveClient(*this); +} + +void +SnapcastClient::LockClose() noexcept +{ + const std::lock_guard protect(output.mutex); + Close(); +} + +void +SnapcastClient::OnSocketReady(unsigned flags) noexcept +{ + if (flags & SocketEvent::WRITE) + // TODO + {} + + BufferedSocket::OnSocketReady(flags); +} + +static bool +Send(SocketDescriptor s, ConstBuffer buffer) noexcept +{ + auto nbytes = s.Write(buffer.data, buffer.size); + return nbytes == ssize_t(buffer.size); +} + +template +static bool +SendT(SocketDescriptor s, const T &buffer) noexcept +{ + return Send(s, ConstBuffer{&buffer, 1}.ToVoid()); +} + +static bool +Send(SocketDescriptor s, StringView buffer) noexcept +{ + return Send(s, buffer.ToVoid()); +} + +static bool +SendServerSettings(SocketDescriptor s, const PackedBE16 id, + const SnapcastBase &request, + const StringView payload) noexcept +{ + const PackedLE32 payload_size = payload.size; + + SnapcastBase base{}; + base.type = uint16_t(SnapcastMessageType::SERVER_SETTINGS); + base.id = id; + base.refers_to = request.id; + base.sent = ToSnapcastTimestamp(std::chrono::steady_clock::now()); + base.size = sizeof(payload_size) + payload.size; + + return SendT(s, base) && SendT(s, payload_size) && Send(s, payload); +} + +bool +SnapcastClient::SendServerSettings(const SnapcastBase &request) noexcept +{ + // TODO: make settings configurable + return ::SendServerSettings(GetSocket(), next_id++, request, + R"({"bufferMs": 1000})"); +} + +static bool +SendCodecHeader(SocketDescriptor s, const PackedBE16 id, + const SnapcastBase &request, + const StringView codec, + const ConstBuffer payload) noexcept +{ + const PackedLE32 codec_size = codec.size; + const PackedLE32 payload_size = payload.size; + + SnapcastBase base{}; + base.type = uint16_t(SnapcastMessageType::CODEC_HEADER); + base.id = id; + base.refers_to = request.id; + base.sent = ToSnapcastTimestamp(std::chrono::steady_clock::now()); + base.size = sizeof(codec_size) + codec.size + + sizeof(payload_size) + payload.size; + + return SendT(s, base) && + SendT(s, codec_size) && Send(s, codec) && + SendT(s, payload_size) && Send(s, payload); +} + +bool +SnapcastClient::SendCodecHeader(const SnapcastBase &request) noexcept +{ + return ::SendCodecHeader(GetSocket(), next_id++, request, + output.GetCodecName(), + output.GetCodecHeader()); +} + +static bool +SendTime(SocketDescriptor s, const PackedBE16 id, + const SnapcastBase &request_header, + const SnapcastTime &request_payload) noexcept +{ + SnapcastTime payload = request_payload; // TODO + + SnapcastBase base{}; + base.type = uint16_t(SnapcastMessageType::TIME); + base.id = id; + base.refers_to = request_header.id; + base.sent = ToSnapcastTimestamp(std::chrono::steady_clock::now()); + base.size = sizeof(payload); + + return SendT(s, base) && SendT(s, payload); +} + +bool +SnapcastClient::SendTime(const SnapcastBase &request_header, + const SnapcastTime &request_payload) noexcept +{ + return ::SendTime(GetSocket(), next_id++, + request_header, request_payload); +} + +static bool +SendWireChunk(SocketDescriptor s, const PackedBE16 id, + const ConstBuffer payload, + std::chrono::steady_clock::time_point t) noexcept +{ + SnapcastWireChunk hdr{}; + hdr.timestamp = ToSnapcastTimestamp(t); + hdr.size = payload.size; + + SnapcastBase base{}; + base.type = uint16_t(SnapcastMessageType::WIRE_CHUNK); + base.id = id; + base.sent = ToSnapcastTimestamp(std::chrono::steady_clock::now()); + base.size = sizeof(hdr) + payload.size; + + // TODO: no blocking send() + return SendT(s, base) && SendT(s, hdr) && Send(s, payload); +} + +void +SnapcastClient::SendWireChunk(ConstBuffer payload, + std::chrono::steady_clock::time_point t) noexcept +{ + if (active) + ::SendWireChunk(GetSocket(), next_id++, payload, t); +} + +BufferedSocket::InputResult +SnapcastClient::OnSocketInput(void *data, size_t length) noexcept +{ + const auto &base = *(const SnapcastBase *)data; + + if (length < sizeof(base) || + length < sizeof(base) + base.size) + return InputResult::MORE; + + ConsumeInput(sizeof(base) + base.size); + + const ConstBuffer payload{&base + 1, base.size}; + + switch (SnapcastMessageType(uint16_t(base.type))) { + case SnapcastMessageType::HELLO: + if (!SendServerSettings(base) || + !SendCodecHeader(base)) { + LockClose(); + return InputResult::CLOSED; + } + + active = true; + break; + + case SnapcastMessageType::TIME: + // TODO: implement this properly + if (payload.size >= sizeof(SnapcastTime)) + SendTime(base, *(const SnapcastTime *)payload.data); + break; + + default: + LockClose(); + return InputResult::CLOSED; + } + + return InputResult::AGAIN; +} + +void +SnapcastClient::OnSocketError(std::exception_ptr ep) noexcept +{ + LogError(ep); + LockClose(); +} + +void +SnapcastClient::OnSocketClosed() noexcept +{ + LockClose(); +} diff --git a/src/output/plugins/snapcast/Client.hxx b/src/output/plugins/snapcast/Client.hxx new file mode 100644 index 000000000..50ff1b992 --- /dev/null +++ b/src/output/plugins/snapcast/Client.hxx @@ -0,0 +1,73 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_SNAPCAST_CLIENT_HXX +#define MPD_OUTPUT_SNAPCAST_CLIENT_HXX + +#include "event/BufferedSocket.hxx" +#include "util/IntrusiveList.hxx" + +#include +#include + +struct SnapcastBase; +struct SnapcastTime; +class SnapcastOutput; +class UniqueSocketDescriptor; + +class SnapcastClient final : BufferedSocket, public IntrusiveListHook +{ + SnapcastOutput &output; + + uint16_t next_id = 1; + + bool active = false; + +public: + SnapcastClient(SnapcastOutput &output, + UniqueSocketDescriptor _fd) noexcept; + + ~SnapcastClient() noexcept; + + /** + * Frees the client and removes it from the server's client list. + * + * Caller must lock the mutex. + */ + void Close() noexcept; + + void LockClose() noexcept; + + void SendWireChunk(ConstBuffer payload, + std::chrono::steady_clock::time_point t) noexcept; + +private: + bool SendServerSettings(const SnapcastBase &request) noexcept; + bool SendCodecHeader(const SnapcastBase &request) noexcept; + bool SendTime(const SnapcastBase &request_header, + const SnapcastTime &request_payload) noexcept; + + /* virtual methods from class BufferedSocket */ + void OnSocketReady(unsigned flags) noexcept override; + InputResult OnSocketInput(void *data, size_t length) noexcept override; + void OnSocketError(std::exception_ptr ep) noexcept override; + void OnSocketClosed() noexcept override; +}; + +#endif diff --git a/src/output/plugins/snapcast/Internal.hxx b/src/output/plugins/snapcast/Internal.hxx new file mode 100644 index 000000000..db11cecf5 --- /dev/null +++ b/src/output/plugins/snapcast/Internal.hxx @@ -0,0 +1,172 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_SNAPCAST_INTERNAL_HXX +#define MPD_OUTPUT_SNAPCAST_INTERNAL_HXX + +#include "output/Interface.hxx" +#include "output/Timer.hxx" +#include "thread/Mutex.hxx" +#include "event/ServerSocket.hxx" +#include "util/AllocatedArray.hxx" +#include "util/IntrusiveList.hxx" + +#include + +struct ConfigBlock; +class SnapcastClient; +class PreparedEncoder; +class Encoder; + +class SnapcastOutput final : AudioOutput, ServerSocket { + /** + * True if the audio output is open and accepts client + * connections. + */ + bool open; + + /** + * The configured encoder plugin. + */ + std::unique_ptr prepared_encoder; + Encoder *encoder = nullptr; + + AllocatedArray codec_header; + + /** + * Number of bytes which were fed into the encoder, without + * ever receiving new output. This is used to estimate + * whether MPD should manually flush the encoder, to avoid + * buffer underruns in the client. + */ + size_t unflushed_input = 0; + + /** + * A #Timer object to synchronize this output with the + * wallclock. + */ + Timer *timer; + + /** + * A linked list containing all clients which are currently + * connected. + */ + IntrusiveList clients; + +public: + /** + * This mutex protects the listener socket and the client + * list. + */ + mutable Mutex mutex; + + SnapcastOutput(EventLoop &_loop, const ConfigBlock &block); + ~SnapcastOutput() noexcept override; + + static AudioOutput *Create(EventLoop &event_loop, + const ConfigBlock &block) { + return new SnapcastOutput(event_loop, block); + } + + using ServerSocket::GetEventLoop; + + void Bind(); + void Unbind() noexcept; + + /** + * Check whether there is at least one client. + * + * Caller must lock the mutex. + */ + [[gnu::pure]] + bool HasClients() const noexcept { + return !clients.empty(); + } + + /** + * Check whether there is at least one client. + */ + [[gnu::pure]] + bool LockHasClients() const noexcept { + const std::lock_guard protect(mutex); + return HasClients(); + } + + /** + * Caller must lock the mutex. + */ + void AddClient(UniqueSocketDescriptor fd) noexcept; + + /** + * Removes a client from the snapcast_output.clients linked list. + * + * Caller must lock the mutex. + */ + void RemoveClient(SnapcastClient &client) noexcept; + + /** + * Caller must lock the mutex. + * + * Throws on error. + */ + void OpenEncoder(AudioFormat &audio_format); + + const char *GetCodecName() const noexcept { + return "pcm"; + } + + ConstBuffer GetCodecHeader() const noexcept { + ConstBuffer result(codec_header); + return result.ToVoid(); + } + + /* virtual methods from class AudioOutput */ + void Enable() override { + Bind(); + } + + void Disable() noexcept override { + Unbind(); + } + + void Open(AudioFormat &audio_format) override; + void Close() noexcept override; + + // TODO: void Interrupt() noexcept override; + + std::chrono::steady_clock::duration Delay() const noexcept override; + + // TODO: void SendTag(const Tag &tag) override; + + size_t Play(const void *chunk, size_t size) override; + + // TODO: void Drain() override; + void Cancel() noexcept override; + bool Pause() override; + +private: + void BroadcastWireChunk(ConstBuffer payload, + std::chrono::steady_clock::time_point t) noexcept; + + /* virtual methods from class ServerSocket */ + void OnAccept(UniqueSocketDescriptor fd, + SocketAddress address, int uid) noexcept override; +}; + +#endif diff --git a/src/output/plugins/snapcast/Protocol.hxx b/src/output/plugins/snapcast/Protocol.hxx new file mode 100644 index 000000000..577ffbecf --- /dev/null +++ b/src/output/plugins/snapcast/Protocol.hxx @@ -0,0 +1,60 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_SNAPCAST_PROTOCOL_HXX +#define MPD_OUTPUT_SNAPCAST_PROTOCOL_HXX + +#include "util/ByteOrder.hxx" + +// see https://github.com/badaix/snapcast/blob/master/doc/binary_protocol.md + +enum class SnapcastMessageType : uint16_t { + CODEC_HEADER = 1, + WIRE_CHUNK = 2, + SERVER_SETTINGS = 3, + TIME = 4, + HELLO = 5, + STREAM_TAGS = 6, +}; + +struct SnapcastTimestamp { + PackedLE32 sec, usec; +}; + +struct SnapcastBase { + PackedLE16 type; + PackedLE16 id; + PackedLE16 refers_to; + SnapcastTimestamp received; + SnapcastTimestamp sent; + PackedLE32 size; +}; + +static_assert(sizeof(SnapcastBase) == 26); + +struct SnapcastWireChunk { + SnapcastTimestamp timestamp; + PackedLE32 size; +}; + +struct SnapcastTime { + SnapcastTimestamp latency; +}; + +#endif diff --git a/src/output/plugins/snapcast/SnapcastOutputPlugin.cxx b/src/output/plugins/snapcast/SnapcastOutputPlugin.cxx new file mode 100644 index 000000000..f80857a80 --- /dev/null +++ b/src/output/plugins/snapcast/SnapcastOutputPlugin.cxx @@ -0,0 +1,262 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "SnapcastOutputPlugin.hxx" +#include "Internal.hxx" +#include "Client.hxx" +#include "output/OutputAPI.hxx" +#include "encoder/EncoderInterface.hxx" +#include "encoder/Configured.hxx" +#include "encoder/plugins/WaveEncoderPlugin.hxx" +#include "net/UniqueSocketDescriptor.hxx" +#include "net/SocketAddress.hxx" +#include "event/Call.hxx" +#include "util/Domain.hxx" +#include "util/DeleteDisposer.hxx" +#include "config/Net.hxx" + +#include + +#include + +inline +SnapcastOutput::SnapcastOutput(EventLoop &_loop, const ConfigBlock &block) + :AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE| + FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT), + ServerSocket(_loop), + // TODO: support other encoder plugins? + prepared_encoder(encoder_init(wave_encoder_plugin, block)) +{ + ServerSocketAddGeneric(*this, block.GetBlockValue("bind_to_address"), + block.GetBlockValue("port", 1704U)); +} + +SnapcastOutput::~SnapcastOutput() noexcept = default; + +inline void +SnapcastOutput::Bind() +{ + open = false; + + BlockingCall(GetEventLoop(), [this](){ + ServerSocket::Open(); + }); + + // TODO: Zeroconf integration +} + +inline void +SnapcastOutput::Unbind() noexcept +{ + assert(!open); + + BlockingCall(GetEventLoop(), [this](){ + ServerSocket::Close(); + }); +} + +/** + * Creates a new #SnapcastClient object and adds it into the + * SnapcastOutput.clients linked list. + */ +inline void +SnapcastOutput::AddClient(UniqueSocketDescriptor fd) noexcept +{ + auto *client = new SnapcastClient(*this, std::move(fd)); + clients.push_front(*client); +} + +void +SnapcastOutput::OnAccept(UniqueSocketDescriptor fd, + SocketAddress, int) noexcept +{ + /* the listener socket has become readable - a client has + connected */ + + const std::lock_guard protect(mutex); + + /* can we allow additional client */ + if (open) + AddClient(std::move(fd)); +} + +static AllocatedArray +ReadEncoder(Encoder &encoder) +{ + std::byte buffer[4096]; + + size_t nbytes = encoder.Read(buffer, sizeof(buffer)); + const ConstBuffer src(buffer, nbytes); + return AllocatedArray{src}; +} + +inline void +SnapcastOutput::OpenEncoder(AudioFormat &audio_format) +{ + encoder = prepared_encoder->Open(audio_format); + + try { + codec_header = ReadEncoder(*encoder); + } catch (...) { + delete encoder; + throw; + } + + unflushed_input = 0; +} + +void +SnapcastOutput::Open(AudioFormat &audio_format) +{ + assert(!open); + assert(clients.empty()); + + const std::lock_guard protect(mutex); + + OpenEncoder(audio_format); + + /* initialize other attributes */ + + timer = new Timer(audio_format); + + open = true; +} + +void +SnapcastOutput::Close() noexcept +{ + assert(open); + + delete timer; + + BlockingCall(GetEventLoop(), [this](){ + const std::lock_guard protect(mutex); + open = false; + clients.clear_and_dispose(DeleteDisposer{}); + }); + + codec_header = nullptr; + delete encoder; +} + +void +SnapcastOutput::RemoveClient(SnapcastClient &client) noexcept +{ + assert(!clients.empty()); + + client.unlink(); + delete &client; +} + +std::chrono::steady_clock::duration +SnapcastOutput::Delay() const noexcept +{ + if (!LockHasClients() /*&& pause*/) { + /* if there's no client and this output is paused, + then Pause() will not do anything, it will not fill + the buffer and it will not update the timer; + therefore, we reset the timer here */ + timer->Reset(); + + /* some arbitrary delay that is long enough to avoid + consuming too much CPU, and short enough to notice + new clients quickly enough */ + return std::chrono::seconds(1); + } + + return timer->IsStarted() + ? timer->GetDelay() + : std::chrono::steady_clock::duration::zero(); +} + +inline void +SnapcastOutput::BroadcastWireChunk(ConstBuffer payload, + std::chrono::steady_clock::time_point t) noexcept +{ + const std::lock_guard protect(mutex); + + // TODO: no blocking send(), enqueue chunks, send() in I/O thread + for (auto &client : clients) + client.SendWireChunk(payload, t); +} + +size_t +SnapcastOutput::Play(const void *chunk, size_t size) +{ + //pause = false; + + const auto now = std::chrono::steady_clock::now(); + + if (!timer->IsStarted()) + timer->Start(); + timer->Add(size); + + if (!LockHasClients()) + return size; + + encoder->Write(chunk, size); + unflushed_input += size; + + if (unflushed_input >= 65536) { + /* we have fed a lot of input into the encoder, but it + didn't give anything back yet - flush now to avoid + buffer underruns */ + try { + encoder->Flush(); + } catch (...) { + /* ignore */ + } + + unflushed_input = 0; + } + + while (true) { + std::byte buffer[32768]; + + size_t nbytes = encoder->Read(buffer, sizeof(buffer)); + if (nbytes == 0) + break; + + BroadcastWireChunk({buffer, nbytes}, now); + } + + return size; +} + +bool +SnapcastOutput::Pause() +{ + // TODO: implement + //pause = true; + + return true; +} + +void +SnapcastOutput::Cancel() noexcept +{ + // TODO +} + +const struct AudioOutputPlugin snapcast_output_plugin = { + "snapcast", + nullptr, + &SnapcastOutput::Create, + nullptr, +}; diff --git a/src/output/plugins/snapcast/SnapcastOutputPlugin.hxx b/src/output/plugins/snapcast/SnapcastOutputPlugin.hxx new file mode 100644 index 000000000..1f72ba4d2 --- /dev/null +++ b/src/output/plugins/snapcast/SnapcastOutputPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_SNAPCAST_OUTPUT_PLUGIN_HXX +#define MPD_SNAPCAST_OUTPUT_PLUGIN_HXX + +extern const struct AudioOutputPlugin snapcast_output_plugin; + +#endif diff --git a/src/output/plugins/snapcast/Timestamp.hxx b/src/output/plugins/snapcast/Timestamp.hxx new file mode 100644 index 000000000..148af8888 --- /dev/null +++ b/src/output/plugins/snapcast/Timestamp.hxx @@ -0,0 +1,45 @@ +/* + * Copyright 2003-2021 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OUTPUT_SNAPCAST_TIMESTAMP_HXX +#define MPD_OUTPUT_SNAPCAST_TIMESTAMP_HXX + +#include "Protocol.hxx" + +#include + +template +static constexpr SnapcastTimestamp +ToSnapcastTimestamp(TimePoint t) noexcept +{ + using Clock = typename TimePoint::clock; + using Duration = typename Clock::duration; + + const auto d = t.time_since_epoch(); + const auto s = std::chrono::duration_cast(d); + const auto rest = d - std::chrono::duration_cast(s); + const auto us = std::chrono::duration_cast(rest); + + SnapcastTimestamp st{}; + st.sec = s.count(); + st.usec = us.count(); + return st; +} + +#endif