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:
parent
85adefd9a4
commit
9c8da03c5c
2
NEWS
2
NEWS
|
@ -1,6 +1,8 @@
|
||||||
ver 0.23 (not yet released)
|
ver 0.23 (not yet released)
|
||||||
* protocol
|
* protocol
|
||||||
- new command "getvol"
|
- new command "getvol"
|
||||||
|
* output
|
||||||
|
- snapcast: new plugin
|
||||||
|
|
||||||
ver 0.22.6 (2021/02/16)
|
ver 0.22.6 (2021/02/16)
|
||||||
* fix missing tags on songs in queue
|
* fix missing tags on songs in queue
|
||||||
|
|
|
@ -1146,6 +1146,29 @@ audio API. Its primary use is local playback on Android, where
|
||||||
floating point samples.
|
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
|
solaris
|
||||||
-------
|
-------
|
||||||
The "Solaris" plugin runs only on SUN Solaris, and plays via /dev/audio.
|
The "Solaris" plugin runs only on SUN Solaris, and plays via /dev/audio.
|
||||||
|
|
|
@ -177,6 +177,7 @@ option('pipe', type: 'boolean', value: true, description: 'Pipe output plugin')
|
||||||
option('pulse', type: 'feature', description: 'PulseAudio support')
|
option('pulse', type: 'feature', description: 'PulseAudio support')
|
||||||
option('recorder', type: 'boolean', value: true, description: 'Recorder output plugin')
|
option('recorder', type: 'boolean', value: true, description: 'Recorder output plugin')
|
||||||
option('shout', type: 'feature', description: 'Shoutcast streaming support using libshout')
|
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('sndio', type: 'feature', description: 'sndio output plugin')
|
||||||
option('solaris_output', type: 'feature', description: 'Solaris /dev/audio support')
|
option('solaris_output', type: 'feature', description: 'Solaris /dev/audio support')
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#include "plugins/AoOutputPlugin.hxx"
|
#include "plugins/AoOutputPlugin.hxx"
|
||||||
#include "plugins/FifoOutputPlugin.hxx"
|
#include "plugins/FifoOutputPlugin.hxx"
|
||||||
#include "plugins/SndioOutputPlugin.hxx"
|
#include "plugins/SndioOutputPlugin.hxx"
|
||||||
|
#include "plugins/snapcast/SnapcastOutputPlugin.hxx"
|
||||||
#include "plugins/httpd/HttpdOutputPlugin.hxx"
|
#include "plugins/httpd/HttpdOutputPlugin.hxx"
|
||||||
#include "plugins/HaikuOutputPlugin.hxx"
|
#include "plugins/HaikuOutputPlugin.hxx"
|
||||||
#include "plugins/JackOutputPlugin.hxx"
|
#include "plugins/JackOutputPlugin.hxx"
|
||||||
|
@ -93,6 +94,9 @@ constexpr const AudioOutputPlugin *audio_output_plugins[] = {
|
||||||
#ifdef ENABLE_HTTPD_OUTPUT
|
#ifdef ENABLE_HTTPD_OUTPUT
|
||||||
&httpd_output_plugin,
|
&httpd_output_plugin,
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef ENABLE_SNAPCAST_OUTPUT
|
||||||
|
&snapcast_output_plugin,
|
||||||
|
#endif
|
||||||
#ifdef ENABLE_RECORDER_OUTPUT
|
#ifdef ENABLE_RECORDER_OUTPUT
|
||||||
&recorder_output_plugin,
|
&recorder_output_plugin,
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -114,6 +114,19 @@ if libsndio_dep.found()
|
||||||
output_plugins_sources += 'SndioOutputPlugin.cxx'
|
output_plugins_sources += 'SndioOutputPlugin.cxx'
|
||||||
endif
|
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')
|
enable_solaris_output = get_option('solaris_output')
|
||||||
if enable_solaris_output.auto()
|
if enable_solaris_output.auto()
|
||||||
enable_solaris_output = host_machine.system() == 'sunos' or host_machine.system() == 'solaris'
|
enable_solaris_output = host_machine.system() == 'sunos' or host_machine.system() == 'solaris'
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||||
|
};
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue