From 9c8da03c5cf7818e48f9077c4d28c554f9f252f4 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Tue, 16 Feb 2021 16:28:41 +0100
Subject: [PATCH] output/snapcast: new output plugin

New experimental code, first draft - it works, but there's a lot left
to do.  Just look at all the TODO comments.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/975
---
 NEWS                                          |   2 +
 doc/plugins.rst                               |  23 ++
 meson_options.txt                             |   1 +
 src/output/Registry.cxx                       |   4 +
 src/output/plugins/meson.build                |  13 +
 src/output/plugins/snapcast/Client.cxx        | 245 ++++++++++++++++
 src/output/plugins/snapcast/Client.hxx        |  73 +++++
 src/output/plugins/snapcast/Internal.hxx      | 172 ++++++++++++
 src/output/plugins/snapcast/Protocol.hxx      |  60 ++++
 .../plugins/snapcast/SnapcastOutputPlugin.cxx | 262 ++++++++++++++++++
 .../plugins/snapcast/SnapcastOutputPlugin.hxx |  25 ++
 src/output/plugins/snapcast/Timestamp.hxx     |  45 +++
 12 files changed, 925 insertions(+)
 create mode 100644 src/output/plugins/snapcast/Client.cxx
 create mode 100644 src/output/plugins/snapcast/Client.hxx
 create mode 100644 src/output/plugins/snapcast/Internal.hxx
 create mode 100644 src/output/plugins/snapcast/Protocol.hxx
 create mode 100644 src/output/plugins/snapcast/SnapcastOutputPlugin.cxx
 create mode 100644 src/output/plugins/snapcast/SnapcastOutputPlugin.hxx
 create mode 100644 src/output/plugins/snapcast/Timestamp.hxx

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
+<https://github.com/badaix/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 <cassert>
+#include <cstring>
+
+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<Mutex> protect(output.mutex);
+	Close();
+}
+
+void
+SnapcastClient::OnSocketReady(unsigned flags) noexcept
+{
+	if (flags & SocketEvent::WRITE)
+		// TODO
+		{}
+
+	BufferedSocket::OnSocketReady(flags);
+}
+
+static bool
+Send(SocketDescriptor s, ConstBuffer<void> buffer) noexcept
+{
+	auto nbytes = s.Write(buffer.data, buffer.size);
+	return nbytes == ssize_t(buffer.size);
+}
+
+template<typename T>
+static bool
+SendT(SocketDescriptor s, const T &buffer) noexcept
+{
+	return Send(s, ConstBuffer<T>{&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<void> 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<void> 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<void> 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<void> 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 <chrono>
+#include <cstdint>
+
+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<void> 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 <memory>
+
+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<PreparedEncoder> prepared_encoder;
+	Encoder *encoder = nullptr;
+
+	AllocatedArray<std::byte> 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<SnapcastClient> 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<Mutex> 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<void> GetCodecHeader() const noexcept {
+		ConstBuffer<std::byte> 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<void> 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 <cassert>
+
+#include <string.h>
+
+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<Mutex> protect(mutex);
+
+	/* can we allow additional client */
+	if (open)
+		AddClient(std::move(fd));
+}
+
+static AllocatedArray<std::byte>
+ReadEncoder(Encoder &encoder)
+{
+	std::byte buffer[4096];
+
+	size_t nbytes = encoder.Read(buffer, sizeof(buffer));
+	const ConstBuffer<std::byte> src(buffer, nbytes);
+	return AllocatedArray<std::byte>{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<Mutex> 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<Mutex> 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<void> payload,
+				   std::chrono::steady_clock::time_point t) noexcept
+{
+	const std::lock_guard<Mutex> 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 <chrono>
+
+template<typename TimePoint>
+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<std::chrono::seconds>(d);
+	const auto rest = d - std::chrono::duration_cast<Duration>(s);
+	const auto us = std::chrono::duration_cast<std::chrono::microseconds>(rest);
+
+	SnapcastTimestamp st{};
+	st.sec = s.count();
+	st.usec = us.count();
+	return st;
+}
+
+#endif