mpd/src/output/plugins/httpd/HttpdOutputPlugin.cxx
2021-01-01 19:54:28 +01:00

413 lines
8.5 KiB
C++

/*
* 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 "HttpdOutputPlugin.hxx"
#include "HttpdInternal.hxx"
#include "HttpdClient.hxx"
#include "output/OutputAPI.hxx"
#include "encoder/EncoderInterface.hxx"
#include "encoder/Configured.hxx"
#include "net/UniqueSocketDescriptor.hxx"
#include "net/SocketAddress.hxx"
#include "Page.hxx"
#include "IcyMetaDataServer.hxx"
#include "event/Call.hxx"
#include "util/Domain.hxx"
#include "util/DeleteDisposer.hxx"
#include "config/Net.hxx"
#include <cassert>
#include <string.h>
const Domain httpd_output_domain("httpd_output");
inline
HttpdOutput::HttpdOutput(EventLoop &_loop, const ConfigBlock &block)
:AudioOutput(FLAG_ENABLE_DISABLE|FLAG_PAUSE),
ServerSocket(_loop),
prepared_encoder(CreateConfiguredEncoder(block)),
defer_broadcast(_loop, BIND_THIS_METHOD(OnDeferredBroadcast))
{
/* read configuration */
name = block.GetBlockValue("name", "Set name in config");
genre = block.GetBlockValue("genre", "Set genre in config");
website = block.GetBlockValue("website", "Set website in config");
clients_max = block.GetBlockValue("max_clients", 0U);
/* set up bind_to_address */
ServerSocketAddGeneric(*this, block.GetBlockValue("bind_to_address"), block.GetBlockValue("port", 8000U));
/* determine content type */
content_type = prepared_encoder->GetMimeType();
if (content_type == nullptr)
content_type = "application/octet-stream";
}
inline void
HttpdOutput::Bind()
{
open = false;
BlockingCall(GetEventLoop(), [this](){
ServerSocket::Open();
});
}
inline void
HttpdOutput::Unbind() noexcept
{
assert(!open);
BlockingCall(GetEventLoop(), [this](){
ServerSocket::Close();
});
}
/**
* Creates a new #HttpdClient object and adds it into the
* HttpdOutput.clients linked list.
*/
inline void
HttpdOutput::AddClient(UniqueSocketDescriptor fd) noexcept
{
auto *client = new HttpdClient(*this, std::move(fd), GetEventLoop(),
!encoder->ImplementsTag());
clients.push_front(*client);
/* pass metadata to client */
if (metadata != nullptr)
clients.front().PushMetaData(metadata);
}
void
HttpdOutput::OnDeferredBroadcast() noexcept
{
/* this method runs in the IOThread; it broadcasts pages from
our own queue to all clients */
const std::lock_guard<Mutex> protect(mutex);
while (!pages.empty()) {
PagePtr page = std::move(pages.front());
pages.pop();
for (auto &client : clients)
client.PushPage(page);
}
/* wake up the client that may be waiting for the queue to be
flushed */
cond.notify_all();
}
void
HttpdOutput::OnAccept(UniqueSocketDescriptor fd,
SocketAddress, [[maybe_unused]] int uid) 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 && (clients_max == 0 || clients.size() < clients_max))
AddClient(std::move(fd));
}
PagePtr
HttpdOutput::ReadPage()
{
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;
}
size_t size = 0;
do {
size_t nbytes = encoder->Read(buffer + size,
sizeof(buffer) - size);
if (nbytes == 0)
break;
unflushed_input = 0;
size += nbytes;
} while (size < sizeof(buffer));
if (size == 0)
return nullptr;
return std::make_shared<Page>(buffer, size);
}
inline void
HttpdOutput::OpenEncoder(AudioFormat &audio_format)
{
encoder = prepared_encoder->Open(audio_format);
/* we have to remember the encoder header, i.e. the first
bytes of encoder output after opening it, because it has to
be sent to every new client */
header = ReadPage();
unflushed_input = 0;
}
void
HttpdOutput::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;
pause = false;
}
void
HttpdOutput::Close() noexcept
{
assert(open);
delete timer;
BlockingCall(GetEventLoop(), [this](){
defer_broadcast.Cancel();
const std::lock_guard<Mutex> protect(mutex);
open = false;
clients.clear_and_dispose(DeleteDisposer());
});
header.reset();
delete encoder;
}
void
HttpdOutput::RemoveClient(HttpdClient &client) noexcept
{
assert(!clients.empty());
clients.erase_and_dispose(clients.iterator_to(client),
DeleteDisposer());
}
void
HttpdOutput::SendHeader(HttpdClient &client) const noexcept
{
if (header != nullptr)
client.PushPage(header);
}
std::chrono::steady_clock::duration
HttpdOutput::Delay() const noexcept
{
if (!LockHasClients() && pause) {
/* if there's no client and this output is paused,
then httpd_output_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();
}
void
HttpdOutput::BroadcastPage(PagePtr page) noexcept
{
assert(page != nullptr);
{
const std::lock_guard<Mutex> lock(mutex);
pages.emplace(std::move(page));
}
defer_broadcast.Schedule();
}
void
HttpdOutput::BroadcastFromEncoder()
{
/* synchronize with the IOThread */
{
std::unique_lock<Mutex> lock(mutex);
cond.wait(lock, [this]{ return pages.empty(); });
}
bool empty = true;
PagePtr page;
while ((page = ReadPage()) != nullptr) {
const std::lock_guard<Mutex> lock(mutex);
pages.emplace(std::move(page));
empty = false;
}
if (!empty)
defer_broadcast.Schedule();
}
inline void
HttpdOutput::EncodeAndPlay(const void *chunk, size_t size)
{
encoder->Write(chunk, size);
unflushed_input += size;
BroadcastFromEncoder();
}
size_t
HttpdOutput::Play(const void *chunk, size_t size)
{
pause = false;
if (LockHasClients())
EncodeAndPlay(chunk, size);
if (!timer->IsStarted())
timer->Start();
timer->Add(size);
return size;
}
bool
HttpdOutput::Pause()
{
pause = true;
if (LockHasClients()) {
static const char silence[1020] = { 0 };
Play(silence, sizeof(silence));
}
return true;
}
void
HttpdOutput::SendTag(const Tag &tag)
{
if (encoder->ImplementsTag()) {
/* embed encoder tags */
/* flush the current stream, and end it */
try {
encoder->PreTag();
} catch (...) {
/* ignore */
}
BroadcastFromEncoder();
/* send the tag to the encoder - which starts a new
stream now */
try {
encoder->SendTag(tag);
encoder->Flush();
} catch (...) {
/* ignore */
}
/* the first page generated by the encoder will now be
used as the new "header" page, which is sent to all
new clients */
auto page = ReadPage();
if (page != nullptr) {
header = page;
BroadcastPage(page);
}
} else {
/* use Icy-Metadata */
static constexpr TagType types[] = {
TAG_ALBUM, TAG_ARTIST, TAG_TITLE,
TAG_NUM_OF_ITEM_TYPES
};
metadata = icy_server_metadata_page(tag, &types[0]);
if (metadata != nullptr) {
const std::lock_guard<Mutex> protect(mutex);
for (auto &client : clients)
client.PushMetaData(metadata);
}
}
}
inline void
HttpdOutput::CancelAllClients() noexcept
{
const std::lock_guard<Mutex> protect(mutex);
while (!pages.empty()) {
PagePtr page = std::move(pages.front());
pages.pop();
}
for (auto &client : clients)
client.CancelQueue();
cond.notify_all();
}
void
HttpdOutput::Cancel() noexcept
{
BlockingCall(GetEventLoop(), [this](){
CancelAllClients();
});
}
const struct AudioOutputPlugin httpd_output_plugin = {
"httpd",
nullptr,
&HttpdOutput::Create,
nullptr,
};