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
This commit is contained in:
Max Kellermann 2021-02-16 16:28:41 +01:00
parent 85adefd9a4
commit 9c8da03c5c
12 changed files with 925 additions and 0 deletions

2
NEWS
View File

@ -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

View File

@ -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.

View File

@ -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')

View File

@ -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

View File

@ -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'

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
};

View File

@ -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

View File

@ -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