output/httpd: move to dedicated directory
This commit is contained in:
485
src/output/plugins/httpd/HttpdClient.cxx
Normal file
485
src/output/plugins/httpd/HttpdClient.cxx
Normal file
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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 "config.h"
|
||||
#include "HttpdClient.hxx"
|
||||
#include "HttpdInternal.hxx"
|
||||
#include "util/ASCII.hxx"
|
||||
#include "Page.hxx"
|
||||
#include "IcyMetaDataServer.hxx"
|
||||
#include "system/SocketError.hxx"
|
||||
#include "Log.hxx"
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
HttpdClient::~HttpdClient()
|
||||
{
|
||||
if (state == RESPONSE) {
|
||||
if (current_page != nullptr)
|
||||
current_page->Unref();
|
||||
|
||||
ClearQueue();
|
||||
}
|
||||
|
||||
if (metadata)
|
||||
metadata->Unref();
|
||||
|
||||
if (IsDefined())
|
||||
BufferedSocket::Close();
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::Close()
|
||||
{
|
||||
httpd.RemoveClient(*this);
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::LockClose()
|
||||
{
|
||||
const ScopeLock protect(httpd.mutex);
|
||||
Close();
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::BeginResponse()
|
||||
{
|
||||
assert(state != RESPONSE);
|
||||
|
||||
state = RESPONSE;
|
||||
current_page = nullptr;
|
||||
|
||||
if (!head_method)
|
||||
httpd.SendHeader(*this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a line of the HTTP request.
|
||||
*/
|
||||
bool
|
||||
HttpdClient::HandleLine(const char *line)
|
||||
{
|
||||
assert(state != RESPONSE);
|
||||
|
||||
if (state == REQUEST) {
|
||||
if (memcmp(line, "HEAD /", 6) == 0) {
|
||||
line += 6;
|
||||
head_method = true;
|
||||
} else if (memcmp(line, "GET /", 5) == 0) {
|
||||
line += 5;
|
||||
} else {
|
||||
/* only GET is supported */
|
||||
LogWarning(httpd_output_domain,
|
||||
"malformed request line from client");
|
||||
return false;
|
||||
}
|
||||
|
||||
line = strchr(line, ' ');
|
||||
if (line == nullptr || memcmp(line + 1, "HTTP/", 5) != 0) {
|
||||
/* HTTP/0.9 without request headers */
|
||||
|
||||
if (head_method)
|
||||
return false;
|
||||
|
||||
BeginResponse();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* after the request line, request headers follow */
|
||||
state = HEADERS;
|
||||
return true;
|
||||
} else {
|
||||
if (*line == 0) {
|
||||
/* empty line: request is finished */
|
||||
|
||||
BeginResponse();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StringEqualsCaseASCII(line, "Icy-MetaData: 1", 15) ||
|
||||
StringEqualsCaseASCII(line, "Icy-MetaData:1", 14)) {
|
||||
/* Send icy metadata */
|
||||
metadata_requested = metadata_supported;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StringEqualsCaseASCII(line, "transferMode.dlna.org: Streaming", 32)) {
|
||||
/* Send as dlna */
|
||||
dlna_streaming_requested = true;
|
||||
/* metadata is not supported by dlna streaming, so disable it */
|
||||
metadata_supported = false;
|
||||
metadata_requested = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* expect more request headers */
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the status line and response headers to the client.
|
||||
*/
|
||||
bool
|
||||
HttpdClient::SendResponse()
|
||||
{
|
||||
char buffer[1024];
|
||||
assert(state == RESPONSE);
|
||||
|
||||
if (dlna_streaming_requested) {
|
||||
snprintf(buffer, sizeof(buffer),
|
||||
"HTTP/1.1 206 OK\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Content-Length: 10000\r\n"
|
||||
"Content-RangeX: 0-1000000/1000000\r\n"
|
||||
"transferMode.dlna.org: Streaming\r\n"
|
||||
"Accept-Ranges: bytes\r\n"
|
||||
"Connection: close\r\n"
|
||||
"realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n"
|
||||
"contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n"
|
||||
"\r\n",
|
||||
httpd.content_type);
|
||||
|
||||
} else if (metadata_requested) {
|
||||
char *metadata_header =
|
||||
icy_server_metadata_header(httpd.name, httpd.genre,
|
||||
httpd.website,
|
||||
httpd.content_type,
|
||||
metaint);
|
||||
|
||||
g_strlcpy(buffer, metadata_header, sizeof(buffer));
|
||||
|
||||
delete[] metadata_header;
|
||||
|
||||
} else { /* revert to a normal HTTP request */
|
||||
snprintf(buffer, sizeof(buffer),
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Pragma: no-cache\r\n"
|
||||
"Cache-Control: no-cache, no-store\r\n"
|
||||
"\r\n",
|
||||
httpd.content_type);
|
||||
}
|
||||
|
||||
ssize_t nbytes = SocketMonitor::Write(buffer, strlen(buffer));
|
||||
if (gcc_unlikely(nbytes < 0)) {
|
||||
const SocketErrorMessage msg;
|
||||
FormatWarning(httpd_output_domain,
|
||||
"failed to write to client: %s",
|
||||
(const char *)msg);
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
HttpdClient::HttpdClient(HttpdOutput &_httpd, int _fd, EventLoop &_loop,
|
||||
bool _metadata_supported)
|
||||
:BufferedSocket(_fd, _loop),
|
||||
httpd(_httpd),
|
||||
state(REQUEST),
|
||||
queue_size(0),
|
||||
head_method(false),
|
||||
dlna_streaming_requested(false),
|
||||
metadata_supported(_metadata_supported),
|
||||
metadata_requested(false), metadata_sent(true),
|
||||
metaint(8192), /*TODO: just a std value */
|
||||
metadata(nullptr),
|
||||
metadata_current_position(0), metadata_fill(0)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::ClearQueue()
|
||||
{
|
||||
assert(state == RESPONSE);
|
||||
|
||||
while (!pages.empty()) {
|
||||
Page *page = pages.front();
|
||||
pages.pop();
|
||||
|
||||
#ifndef NDEBUG
|
||||
assert(queue_size >= page->size);
|
||||
queue_size -= page->size;
|
||||
#endif
|
||||
|
||||
page->Unref();
|
||||
}
|
||||
|
||||
assert(queue_size == 0);
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::CancelQueue()
|
||||
{
|
||||
if (state != RESPONSE)
|
||||
return;
|
||||
|
||||
ClearQueue();
|
||||
|
||||
if (current_page == nullptr)
|
||||
CancelWrite();
|
||||
}
|
||||
|
||||
ssize_t
|
||||
HttpdClient::TryWritePage(const Page &page, size_t position)
|
||||
{
|
||||
assert(position < page.size);
|
||||
|
||||
return Write(page.data + position, page.size - position);
|
||||
}
|
||||
|
||||
ssize_t
|
||||
HttpdClient::TryWritePageN(const Page &page, size_t position, ssize_t n)
|
||||
{
|
||||
return n >= 0
|
||||
? Write(page.data + position, n)
|
||||
: TryWritePage(page, position);
|
||||
}
|
||||
|
||||
ssize_t
|
||||
HttpdClient::GetBytesTillMetaData() const
|
||||
{
|
||||
if (metadata_requested &&
|
||||
current_page->size - current_position > metaint - metadata_fill)
|
||||
return metaint - metadata_fill;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline bool
|
||||
HttpdClient::TryWrite()
|
||||
{
|
||||
const ScopeLock protect(httpd.mutex);
|
||||
|
||||
assert(state == RESPONSE);
|
||||
|
||||
if (current_page == nullptr) {
|
||||
if (pages.empty()) {
|
||||
/* another thread has removed the event source
|
||||
while this thread was waiting for
|
||||
httpd.mutex */
|
||||
CancelWrite();
|
||||
return true;
|
||||
}
|
||||
|
||||
current_page = pages.front();
|
||||
pages.pop();
|
||||
current_position = 0;
|
||||
|
||||
assert(queue_size >= current_page->size);
|
||||
queue_size -= current_page->size;
|
||||
}
|
||||
|
||||
const ssize_t bytes_to_write = GetBytesTillMetaData();
|
||||
if (bytes_to_write == 0) {
|
||||
if (!metadata_sent) {
|
||||
ssize_t nbytes = TryWritePage(*metadata,
|
||||
metadata_current_position);
|
||||
if (nbytes < 0) {
|
||||
auto e = GetSocketError();
|
||||
if (IsSocketErrorAgain(e))
|
||||
return true;
|
||||
|
||||
if (!IsSocketErrorClosed(e)) {
|
||||
SocketErrorMessage msg(e);
|
||||
FormatWarning(httpd_output_domain,
|
||||
"failed to write to client: %s",
|
||||
(const char *)msg);
|
||||
}
|
||||
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
metadata_current_position += nbytes;
|
||||
|
||||
if (metadata->size - metadata_current_position == 0) {
|
||||
metadata_fill = 0;
|
||||
metadata_current_position = 0;
|
||||
metadata_sent = true;
|
||||
}
|
||||
} else {
|
||||
guchar empty_data = 0;
|
||||
|
||||
ssize_t nbytes = Write(&empty_data, 1);
|
||||
if (nbytes < 0) {
|
||||
auto e = GetSocketError();
|
||||
if (IsSocketErrorAgain(e))
|
||||
return true;
|
||||
|
||||
if (!IsSocketErrorClosed(e)) {
|
||||
SocketErrorMessage msg(e);
|
||||
FormatWarning(httpd_output_domain,
|
||||
"failed to write to client: %s",
|
||||
(const char *)msg);
|
||||
}
|
||||
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
metadata_fill = 0;
|
||||
metadata_current_position = 0;
|
||||
}
|
||||
} else {
|
||||
ssize_t nbytes =
|
||||
TryWritePageN(*current_page, current_position,
|
||||
bytes_to_write);
|
||||
if (nbytes < 0) {
|
||||
auto e = GetSocketError();
|
||||
if (IsSocketErrorAgain(e))
|
||||
return true;
|
||||
|
||||
if (!IsSocketErrorClosed(e)) {
|
||||
SocketErrorMessage msg(e);
|
||||
FormatWarning(httpd_output_domain,
|
||||
"failed to write to client: %s",
|
||||
(const char *)msg);
|
||||
}
|
||||
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
current_position += nbytes;
|
||||
assert(current_position <= current_page->size);
|
||||
|
||||
if (metadata_requested)
|
||||
metadata_fill += nbytes;
|
||||
|
||||
if (current_position >= current_page->size) {
|
||||
current_page->Unref();
|
||||
current_page = nullptr;
|
||||
|
||||
if (pages.empty())
|
||||
/* all pages are sent: remove the
|
||||
event source */
|
||||
CancelWrite();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::PushPage(Page *page)
|
||||
{
|
||||
if (state != RESPONSE)
|
||||
/* the client is still writing the HTTP request */
|
||||
return;
|
||||
|
||||
if (queue_size > 256 * 1024) {
|
||||
FormatDebug(httpd_output_domain,
|
||||
"client is too slow, flushing its queue");
|
||||
ClearQueue();
|
||||
}
|
||||
|
||||
page->Ref();
|
||||
pages.push(page);
|
||||
queue_size += page->size;
|
||||
|
||||
ScheduleWrite();
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::PushMetaData(Page *page)
|
||||
{
|
||||
if (metadata) {
|
||||
metadata->Unref();
|
||||
metadata = nullptr;
|
||||
}
|
||||
|
||||
g_return_if_fail (page);
|
||||
|
||||
page->Ref();
|
||||
metadata = page;
|
||||
metadata_sent = false;
|
||||
}
|
||||
|
||||
bool
|
||||
HttpdClient::OnSocketReady(unsigned flags)
|
||||
{
|
||||
if (!BufferedSocket::OnSocketReady(flags))
|
||||
return false;
|
||||
|
||||
if (flags & WRITE)
|
||||
if (!TryWrite())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
BufferedSocket::InputResult
|
||||
HttpdClient::OnSocketInput(void *data, size_t length)
|
||||
{
|
||||
if (state == RESPONSE) {
|
||||
LogWarning(httpd_output_domain,
|
||||
"unexpected input from client");
|
||||
LockClose();
|
||||
return InputResult::CLOSED;
|
||||
}
|
||||
|
||||
char *line = (char *)data;
|
||||
char *newline = (char *)memchr(line, '\n', length);
|
||||
if (newline == nullptr)
|
||||
return InputResult::MORE;
|
||||
|
||||
ConsumeInput(newline + 1 - line);
|
||||
|
||||
if (newline > line && newline[-1] == '\r')
|
||||
--newline;
|
||||
|
||||
/* terminate the string at the end of the line */
|
||||
*newline = 0;
|
||||
|
||||
if (!HandleLine(line)) {
|
||||
LockClose();
|
||||
return InputResult::CLOSED;
|
||||
}
|
||||
|
||||
if (state == RESPONSE) {
|
||||
if (!SendResponse())
|
||||
return InputResult::CLOSED;
|
||||
|
||||
if (head_method) {
|
||||
LockClose();
|
||||
return InputResult::CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
return InputResult::AGAIN;
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::OnSocketError(Error &&error)
|
||||
{
|
||||
LogError(error);
|
||||
}
|
||||
|
||||
void
|
||||
HttpdClient::OnSocketClosed()
|
||||
{
|
||||
LockClose();
|
||||
}
|
||||
193
src/output/plugins/httpd/HttpdClient.hxx
Normal file
193
src/output/plugins/httpd/HttpdClient.hxx
Normal file
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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_HTTPD_CLIENT_HXX
|
||||
#define MPD_OUTPUT_HTTPD_CLIENT_HXX
|
||||
|
||||
#include "event/BufferedSocket.hxx"
|
||||
#include "Compiler.h"
|
||||
|
||||
#include <queue>
|
||||
#include <list>
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
class HttpdOutput;
|
||||
class Page;
|
||||
|
||||
class HttpdClient final : BufferedSocket {
|
||||
/**
|
||||
* The httpd output object this client is connected to.
|
||||
*/
|
||||
HttpdOutput &httpd;
|
||||
|
||||
/**
|
||||
* The current state of the client.
|
||||
*/
|
||||
enum {
|
||||
/** reading the request line */
|
||||
REQUEST,
|
||||
|
||||
/** reading the request headers */
|
||||
HEADERS,
|
||||
|
||||
/** sending the HTTP response */
|
||||
RESPONSE,
|
||||
} state;
|
||||
|
||||
/**
|
||||
* A queue of #Page objects to be sent to the client.
|
||||
*/
|
||||
std::queue<Page *, std::list<Page *>> pages;
|
||||
|
||||
/**
|
||||
* The sum of all page sizes in #pages.
|
||||
*/
|
||||
size_t queue_size;
|
||||
|
||||
/**
|
||||
* The #page which is currently being sent to the client.
|
||||
*/
|
||||
Page *current_page;
|
||||
|
||||
/**
|
||||
* The amount of bytes which were already sent from
|
||||
* #current_page.
|
||||
*/
|
||||
size_t current_position;
|
||||
|
||||
/**
|
||||
* Is this a HEAD request?
|
||||
*/
|
||||
bool head_method;
|
||||
|
||||
/**
|
||||
* If DLNA streaming was an option.
|
||||
*/
|
||||
bool dlna_streaming_requested;
|
||||
|
||||
/* ICY */
|
||||
|
||||
/**
|
||||
* Do we support sending Icy-Metadata to the client? This is
|
||||
* disabled if the httpd audio output uses encoder tags.
|
||||
*/
|
||||
bool metadata_supported;
|
||||
|
||||
/**
|
||||
* If we should sent icy metadata.
|
||||
*/
|
||||
bool metadata_requested;
|
||||
|
||||
/**
|
||||
* If the current metadata was already sent to the client.
|
||||
*/
|
||||
bool metadata_sent;
|
||||
|
||||
/**
|
||||
* The amount of streaming data between each metadata block
|
||||
*/
|
||||
unsigned metaint;
|
||||
|
||||
/**
|
||||
* The metadata as #Page which is currently being sent to the client.
|
||||
*/
|
||||
Page *metadata;
|
||||
|
||||
/*
|
||||
* The amount of bytes which were already sent from the metadata.
|
||||
*/
|
||||
size_t metadata_current_position;
|
||||
|
||||
/**
|
||||
* The amount of streaming data sent to the client
|
||||
* since the last icy information was sent.
|
||||
*/
|
||||
unsigned metadata_fill;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @param httpd the HTTP output device
|
||||
* @param fd the socket file descriptor
|
||||
*/
|
||||
HttpdClient(HttpdOutput &httpd, int _fd, EventLoop &_loop,
|
||||
bool _metadata_supported);
|
||||
|
||||
/**
|
||||
* Note: this does not remove the client from the
|
||||
* #HttpdOutput object.
|
||||
*/
|
||||
~HttpdClient();
|
||||
|
||||
/**
|
||||
* Frees the client and removes it from the server's client list.
|
||||
*/
|
||||
void Close();
|
||||
|
||||
void LockClose();
|
||||
|
||||
/**
|
||||
* Clears the page queue.
|
||||
*/
|
||||
void CancelQueue();
|
||||
|
||||
/**
|
||||
* Handle a line of the HTTP request.
|
||||
*/
|
||||
bool HandleLine(const char *line);
|
||||
|
||||
/**
|
||||
* Switch the client to the "RESPONSE" state.
|
||||
*/
|
||||
void BeginResponse();
|
||||
|
||||
/**
|
||||
* Sends the status line and response headers to the client.
|
||||
*/
|
||||
bool SendResponse();
|
||||
|
||||
gcc_pure
|
||||
ssize_t GetBytesTillMetaData() const;
|
||||
|
||||
ssize_t TryWritePage(const Page &page, size_t position);
|
||||
ssize_t TryWritePageN(const Page &page, size_t position, ssize_t n);
|
||||
|
||||
bool TryWrite();
|
||||
|
||||
/**
|
||||
* Appends a page to the client's queue.
|
||||
*/
|
||||
void PushPage(Page *page);
|
||||
|
||||
/**
|
||||
* Sends the passed metadata.
|
||||
*/
|
||||
void PushMetaData(Page *page);
|
||||
|
||||
private:
|
||||
void ClearQueue();
|
||||
|
||||
protected:
|
||||
virtual bool OnSocketReady(unsigned flags) override;
|
||||
virtual InputResult OnSocketInput(void *data, size_t length) override;
|
||||
virtual void OnSocketError(Error &&error) override;
|
||||
virtual void OnSocketClosed() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
273
src/output/plugins/httpd/HttpdInternal.hxx
Normal file
273
src/output/plugins/httpd/HttpdInternal.hxx
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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.
|
||||
*/
|
||||
|
||||
/** \file
|
||||
*
|
||||
* Internal declarations for the "httpd" audio output plugin.
|
||||
*/
|
||||
|
||||
#ifndef MPD_OUTPUT_HTTPD_INTERNAL_H
|
||||
#define MPD_OUTPUT_HTTPD_INTERNAL_H
|
||||
|
||||
#include "output/Internal.hxx"
|
||||
#include "output/Timer.hxx"
|
||||
#include "thread/Mutex.hxx"
|
||||
#include "event/ServerSocket.hxx"
|
||||
#include "event/DeferredMonitor.hxx"
|
||||
#include "util/Cast.hxx"
|
||||
|
||||
#ifdef _LIBCPP_VERSION
|
||||
/* can't use incomplete template arguments with libc++ */
|
||||
#include "HttpdClient.hxx"
|
||||
#endif
|
||||
|
||||
#include <forward_list>
|
||||
#include <queue>
|
||||
#include <list>
|
||||
|
||||
struct config_param;
|
||||
class Error;
|
||||
class EventLoop;
|
||||
class ServerSocket;
|
||||
class HttpdClient;
|
||||
class Page;
|
||||
struct Encoder;
|
||||
struct Tag;
|
||||
|
||||
class HttpdOutput final : ServerSocket, DeferredMonitor {
|
||||
AudioOutput base;
|
||||
|
||||
/**
|
||||
* True if the audio output is open and accepts client
|
||||
* connections.
|
||||
*/
|
||||
bool open;
|
||||
|
||||
/**
|
||||
* The configured encoder plugin.
|
||||
*/
|
||||
Encoder *encoder;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public:
|
||||
/**
|
||||
* The MIME type produced by the #encoder.
|
||||
*/
|
||||
const char *content_type;
|
||||
|
||||
/**
|
||||
* This mutex protects the listener socket and the client
|
||||
* list.
|
||||
*/
|
||||
mutable Mutex mutex;
|
||||
|
||||
/**
|
||||
* This condition gets signalled when an item is removed from
|
||||
* #pages.
|
||||
*/
|
||||
Cond cond;
|
||||
|
||||
private:
|
||||
/**
|
||||
* A #Timer object to synchronize this output with the
|
||||
* wallclock.
|
||||
*/
|
||||
Timer *timer;
|
||||
|
||||
/**
|
||||
* The header page, which is sent to every client on connect.
|
||||
*/
|
||||
Page *header;
|
||||
|
||||
/**
|
||||
* The metadata, which is sent to every client.
|
||||
*/
|
||||
Page *metadata;
|
||||
|
||||
/**
|
||||
* The page queue, i.e. pages from the encoder to be
|
||||
* broadcasted to all clients. This container is necessary to
|
||||
* pass pages from the OutputThread to the IOThread. It is
|
||||
* protected by #mutex, and removing signals #cond.
|
||||
*/
|
||||
std::queue<Page *, std::list<Page *>> pages;
|
||||
|
||||
public:
|
||||
/**
|
||||
* The configured name.
|
||||
*/
|
||||
char const *name;
|
||||
/**
|
||||
* The configured genre.
|
||||
*/
|
||||
char const *genre;
|
||||
/**
|
||||
* The configured website address.
|
||||
*/
|
||||
char const *website;
|
||||
|
||||
private:
|
||||
/**
|
||||
* A linked list containing all clients which are currently
|
||||
* connected.
|
||||
*/
|
||||
std::forward_list<HttpdClient> clients;
|
||||
|
||||
/**
|
||||
* A temporary buffer for the httpd_output_read_page()
|
||||
* function.
|
||||
*/
|
||||
char buffer[32768];
|
||||
|
||||
/**
|
||||
* The maximum and current number of clients connected
|
||||
* at the same time.
|
||||
*/
|
||||
unsigned clients_max, clients_cnt;
|
||||
|
||||
public:
|
||||
HttpdOutput(EventLoop &_loop);
|
||||
~HttpdOutput();
|
||||
|
||||
#if GCC_CHECK_VERSION(4,6) || defined(__clang__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
|
||||
#endif
|
||||
|
||||
static constexpr HttpdOutput *Cast(AudioOutput *ao) {
|
||||
return ContainerCast(ao, HttpdOutput, base);
|
||||
}
|
||||
|
||||
#if GCC_CHECK_VERSION(4,6) || defined(__clang__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
using DeferredMonitor::GetEventLoop;
|
||||
|
||||
bool Init(const config_param ¶m, Error &error);
|
||||
|
||||
bool Configure(const config_param ¶m, Error &error);
|
||||
|
||||
AudioOutput *InitAndConfigure(const config_param ¶m,
|
||||
Error &error) {
|
||||
if (!Init(param, error))
|
||||
return nullptr;
|
||||
|
||||
if (!Configure(param, error))
|
||||
return nullptr;
|
||||
|
||||
return &base;
|
||||
}
|
||||
|
||||
bool Bind(Error &error);
|
||||
void Unbind();
|
||||
|
||||
/**
|
||||
* Caller must lock the mutex.
|
||||
*/
|
||||
bool OpenEncoder(AudioFormat &audio_format, Error &error);
|
||||
|
||||
/**
|
||||
* Caller must lock the mutex.
|
||||
*/
|
||||
bool Open(AudioFormat &audio_format, Error &error);
|
||||
|
||||
/**
|
||||
* Caller must lock the mutex.
|
||||
*/
|
||||
void Close();
|
||||
|
||||
/**
|
||||
* Check whether there is at least one client.
|
||||
*
|
||||
* Caller must lock the mutex.
|
||||
*/
|
||||
gcc_pure
|
||||
bool HasClients() const {
|
||||
return !clients.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is at least one client.
|
||||
*/
|
||||
gcc_pure
|
||||
bool LockHasClients() const {
|
||||
const ScopeLock protect(mutex);
|
||||
return HasClients();
|
||||
}
|
||||
|
||||
void AddClient(int fd);
|
||||
|
||||
/**
|
||||
* Removes a client from the httpd_output.clients linked list.
|
||||
*/
|
||||
void RemoveClient(HttpdClient &client);
|
||||
|
||||
/**
|
||||
* Sends the encoder header to the client. This is called
|
||||
* right after the response headers have been sent.
|
||||
*/
|
||||
void SendHeader(HttpdClient &client) const;
|
||||
|
||||
gcc_pure
|
||||
unsigned Delay() const;
|
||||
|
||||
/**
|
||||
* Reads data from the encoder (as much as available) and
|
||||
* returns it as a new #page object.
|
||||
*/
|
||||
Page *ReadPage();
|
||||
|
||||
/**
|
||||
* Broadcasts a page struct to all clients.
|
||||
*
|
||||
* Mutext must not be locked.
|
||||
*/
|
||||
void BroadcastPage(Page *page);
|
||||
|
||||
/**
|
||||
* Broadcasts data from the encoder to all clients.
|
||||
*/
|
||||
void BroadcastFromEncoder();
|
||||
|
||||
bool EncodeAndPlay(const void *chunk, size_t size, Error &error);
|
||||
|
||||
void SendTag(const Tag *tag);
|
||||
|
||||
size_t Play(const void *chunk, size_t size, Error &error);
|
||||
|
||||
void CancelAllClients();
|
||||
|
||||
private:
|
||||
virtual void RunDeferred() override;
|
||||
|
||||
virtual void OnAccept(int fd, const sockaddr &address,
|
||||
size_t address_length, int uid) override;
|
||||
};
|
||||
|
||||
extern const class Domain httpd_output_domain;
|
||||
|
||||
#endif
|
||||
601
src/output/plugins/httpd/HttpdOutputPlugin.cxx
Normal file
601
src/output/plugins/httpd/HttpdOutputPlugin.cxx
Normal file
@@ -0,0 +1,601 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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 "config.h"
|
||||
#include "HttpdOutputPlugin.hxx"
|
||||
#include "HttpdInternal.hxx"
|
||||
#include "HttpdClient.hxx"
|
||||
#include "output/OutputAPI.hxx"
|
||||
#include "encoder/EncoderPlugin.hxx"
|
||||
#include "encoder/EncoderList.hxx"
|
||||
#include "system/Resolver.hxx"
|
||||
#include "Page.hxx"
|
||||
#include "IcyMetaDataServer.hxx"
|
||||
#include "system/fd_util.h"
|
||||
#include "IOThread.hxx"
|
||||
#include "event/Call.hxx"
|
||||
#include "util/Error.hxx"
|
||||
#include "util/Domain.hxx"
|
||||
#include "Log.hxx"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef HAVE_LIBWRAP
|
||||
#include <sys/socket.h> /* needed for AF_UNIX */
|
||||
#include <tcpd.h>
|
||||
#endif
|
||||
|
||||
const Domain httpd_output_domain("httpd_output");
|
||||
|
||||
inline
|
||||
HttpdOutput::HttpdOutput(EventLoop &_loop)
|
||||
:ServerSocket(_loop), DeferredMonitor(_loop),
|
||||
base(httpd_output_plugin),
|
||||
encoder(nullptr), unflushed_input(0),
|
||||
metadata(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
HttpdOutput::~HttpdOutput()
|
||||
{
|
||||
if (metadata != nullptr)
|
||||
metadata->Unref();
|
||||
|
||||
if (encoder != nullptr)
|
||||
encoder_finish(encoder);
|
||||
|
||||
}
|
||||
|
||||
inline bool
|
||||
HttpdOutput::Bind(Error &error)
|
||||
{
|
||||
open = false;
|
||||
|
||||
bool result = false;
|
||||
BlockingCall(GetEventLoop(), [this, &error, &result](){
|
||||
result = ServerSocket::Open(error);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
inline void
|
||||
HttpdOutput::Unbind()
|
||||
{
|
||||
assert(!open);
|
||||
|
||||
BlockingCall(GetEventLoop(), [this](){
|
||||
ServerSocket::Close();
|
||||
});
|
||||
}
|
||||
|
||||
inline bool
|
||||
HttpdOutput::Configure(const config_param ¶m, Error &error)
|
||||
{
|
||||
/* read configuration */
|
||||
name = param.GetBlockValue("name", "Set name in config");
|
||||
genre = param.GetBlockValue("genre", "Set genre in config");
|
||||
website = param.GetBlockValue("website", "Set website in config");
|
||||
|
||||
unsigned port = param.GetBlockValue("port", 8000u);
|
||||
|
||||
const char *encoder_name =
|
||||
param.GetBlockValue("encoder", "vorbis");
|
||||
const auto encoder_plugin = encoder_plugin_get(encoder_name);
|
||||
if (encoder_plugin == nullptr) {
|
||||
error.Format(httpd_output_domain,
|
||||
"No such encoder: %s", encoder_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
clients_max = param.GetBlockValue("max_clients", 0u);
|
||||
|
||||
/* set up bind_to_address */
|
||||
|
||||
const char *bind_to_address = param.GetBlockValue("bind_to_address");
|
||||
bool success = bind_to_address != nullptr &&
|
||||
strcmp(bind_to_address, "any") != 0
|
||||
? AddHost(bind_to_address, port, error)
|
||||
: AddPort(port, error);
|
||||
if (!success)
|
||||
return false;
|
||||
|
||||
/* initialize encoder */
|
||||
|
||||
encoder = encoder_init(*encoder_plugin, param, error);
|
||||
if (encoder == nullptr)
|
||||
return false;
|
||||
|
||||
/* determine content type */
|
||||
content_type = encoder_get_mime_type(encoder);
|
||||
if (content_type == nullptr)
|
||||
content_type = "application/octet-stream";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool
|
||||
HttpdOutput::Init(const config_param ¶m, Error &error)
|
||||
{
|
||||
return base.Configure(param, error);
|
||||
}
|
||||
|
||||
static AudioOutput *
|
||||
httpd_output_init(const config_param ¶m, Error &error)
|
||||
{
|
||||
HttpdOutput *httpd = new HttpdOutput(io_thread_get());
|
||||
|
||||
AudioOutput *result = httpd->InitAndConfigure(param, error);
|
||||
if (result == nullptr)
|
||||
delete httpd;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
httpd_output_finish(AudioOutput *ao)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
delete httpd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new #HttpdClient object and adds it into the
|
||||
* HttpdOutput.clients linked list.
|
||||
*/
|
||||
inline void
|
||||
HttpdOutput::AddClient(int fd)
|
||||
{
|
||||
clients.emplace_front(*this, fd, GetEventLoop(),
|
||||
encoder->plugin.tag == nullptr);
|
||||
++clients_cnt;
|
||||
|
||||
/* pass metadata to client */
|
||||
if (metadata != nullptr)
|
||||
clients.front().PushMetaData(metadata);
|
||||
}
|
||||
|
||||
void
|
||||
HttpdOutput::RunDeferred()
|
||||
{
|
||||
/* this method runs in the IOThread; it broadcasts pages from
|
||||
our own queue to all clients */
|
||||
|
||||
const ScopeLock protect(mutex);
|
||||
|
||||
while (!pages.empty()) {
|
||||
Page *page = pages.front();
|
||||
pages.pop();
|
||||
|
||||
for (auto &client : clients)
|
||||
client.PushPage(page);
|
||||
|
||||
page->Unref();
|
||||
}
|
||||
|
||||
/* wake up the client that may be waiting for the queue to be
|
||||
flushed */
|
||||
cond.broadcast();
|
||||
}
|
||||
|
||||
void
|
||||
HttpdOutput::OnAccept(int fd, const sockaddr &address,
|
||||
size_t address_length, gcc_unused int uid)
|
||||
{
|
||||
/* the listener socket has become readable - a client has
|
||||
connected */
|
||||
|
||||
#ifdef HAVE_LIBWRAP
|
||||
if (address.sa_family != AF_UNIX) {
|
||||
const auto hostaddr = sockaddr_to_string(&address,
|
||||
address_length);
|
||||
// TODO: shall we obtain the program name from argv[0]?
|
||||
const char *progname = "mpd";
|
||||
|
||||
struct request_info req;
|
||||
request_init(&req, RQ_FILE, fd, RQ_DAEMON, progname, 0);
|
||||
|
||||
fromhost(&req);
|
||||
|
||||
if (!hosts_access(&req)) {
|
||||
/* tcp wrappers says no */
|
||||
FormatWarning(httpd_output_domain,
|
||||
"libwrap refused connection (libwrap=%s) from %s",
|
||||
progname, hostaddr.c_str());
|
||||
close_socket(fd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)address;
|
||||
(void)address_length;
|
||||
#endif /* HAVE_WRAP */
|
||||
|
||||
const ScopeLock protect(mutex);
|
||||
|
||||
if (fd >= 0) {
|
||||
/* can we allow additional client */
|
||||
if (open && (clients_max == 0 || clients_cnt < clients_max))
|
||||
AddClient(fd);
|
||||
else
|
||||
close_socket(fd);
|
||||
} else if (fd < 0 && errno != EINTR) {
|
||||
LogErrno(httpd_output_domain, "accept() failed");
|
||||
}
|
||||
}
|
||||
|
||||
Page *
|
||||
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 */
|
||||
encoder_flush(encoder, IgnoreError());
|
||||
unflushed_input = 0;
|
||||
}
|
||||
|
||||
size_t size = 0;
|
||||
do {
|
||||
size_t nbytes = encoder_read(encoder,
|
||||
buffer + size,
|
||||
sizeof(buffer) - size);
|
||||
if (nbytes == 0)
|
||||
break;
|
||||
|
||||
unflushed_input = 0;
|
||||
|
||||
size += nbytes;
|
||||
} while (size < sizeof(buffer));
|
||||
|
||||
if (size == 0)
|
||||
return nullptr;
|
||||
|
||||
return Page::Copy(buffer, size);
|
||||
}
|
||||
|
||||
static bool
|
||||
httpd_output_enable(AudioOutput *ao, Error &error)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
return httpd->Bind(error);
|
||||
}
|
||||
|
||||
static void
|
||||
httpd_output_disable(AudioOutput *ao)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
httpd->Unbind();
|
||||
}
|
||||
|
||||
inline bool
|
||||
HttpdOutput::OpenEncoder(AudioFormat &audio_format, Error &error)
|
||||
{
|
||||
if (!encoder_open(encoder, audio_format, error))
|
||||
return false;
|
||||
|
||||
/* 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;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool
|
||||
HttpdOutput::Open(AudioFormat &audio_format, Error &error)
|
||||
{
|
||||
assert(!open);
|
||||
assert(clients.empty());
|
||||
|
||||
/* open the encoder */
|
||||
|
||||
if (!OpenEncoder(audio_format, error))
|
||||
return false;
|
||||
|
||||
/* initialize other attributes */
|
||||
|
||||
clients_cnt = 0;
|
||||
timer = new Timer(audio_format);
|
||||
|
||||
open = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
httpd_output_open(AudioOutput *ao, AudioFormat &audio_format,
|
||||
Error &error)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
const ScopeLock protect(httpd->mutex);
|
||||
return httpd->Open(audio_format, error);
|
||||
}
|
||||
|
||||
inline void
|
||||
HttpdOutput::Close()
|
||||
{
|
||||
assert(open);
|
||||
|
||||
open = false;
|
||||
|
||||
delete timer;
|
||||
|
||||
BlockingCall(GetEventLoop(), [this](){
|
||||
clients.clear();
|
||||
});
|
||||
|
||||
if (header != nullptr)
|
||||
header->Unref();
|
||||
|
||||
encoder_close(encoder);
|
||||
}
|
||||
|
||||
static void
|
||||
httpd_output_close(AudioOutput *ao)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
const ScopeLock protect(httpd->mutex);
|
||||
httpd->Close();
|
||||
}
|
||||
|
||||
void
|
||||
HttpdOutput::RemoveClient(HttpdClient &client)
|
||||
{
|
||||
assert(clients_cnt > 0);
|
||||
|
||||
for (auto prev = clients.before_begin(), i = std::next(prev);;
|
||||
prev = i, i = std::next(prev)) {
|
||||
assert(i != clients.end());
|
||||
if (&*i == &client) {
|
||||
clients.erase_after(prev);
|
||||
clients_cnt--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
HttpdOutput::SendHeader(HttpdClient &client) const
|
||||
{
|
||||
if (header != nullptr)
|
||||
client.PushPage(header);
|
||||
}
|
||||
|
||||
inline unsigned
|
||||
HttpdOutput::Delay() const
|
||||
{
|
||||
if (!LockHasClients() && base.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 1000;
|
||||
}
|
||||
|
||||
return timer->IsStarted()
|
||||
? timer->GetDelay()
|
||||
: 0;
|
||||
}
|
||||
|
||||
static unsigned
|
||||
httpd_output_delay(AudioOutput *ao)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
return httpd->Delay();
|
||||
}
|
||||
|
||||
void
|
||||
HttpdOutput::BroadcastPage(Page *page)
|
||||
{
|
||||
assert(page != nullptr);
|
||||
|
||||
mutex.lock();
|
||||
pages.push(page);
|
||||
page->Ref();
|
||||
mutex.unlock();
|
||||
|
||||
DeferredMonitor::Schedule();
|
||||
}
|
||||
|
||||
void
|
||||
HttpdOutput::BroadcastFromEncoder()
|
||||
{
|
||||
/* synchronize with the IOThread */
|
||||
mutex.lock();
|
||||
while (!pages.empty())
|
||||
cond.wait(mutex);
|
||||
|
||||
Page *page;
|
||||
while ((page = ReadPage()) != nullptr)
|
||||
pages.push(page);
|
||||
|
||||
mutex.unlock();
|
||||
|
||||
DeferredMonitor::Schedule();
|
||||
}
|
||||
|
||||
inline bool
|
||||
HttpdOutput::EncodeAndPlay(const void *chunk, size_t size, Error &error)
|
||||
{
|
||||
if (!encoder_write(encoder, chunk, size, error))
|
||||
return false;
|
||||
|
||||
unflushed_input += size;
|
||||
|
||||
BroadcastFromEncoder();
|
||||
return true;
|
||||
}
|
||||
|
||||
inline size_t
|
||||
HttpdOutput::Play(const void *chunk, size_t size, Error &error)
|
||||
{
|
||||
if (LockHasClients()) {
|
||||
if (!EncodeAndPlay(chunk, size, error))
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!timer->IsStarted())
|
||||
timer->Start();
|
||||
timer->Add(size);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
static size_t
|
||||
httpd_output_play(AudioOutput *ao, const void *chunk, size_t size,
|
||||
Error &error)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
return httpd->Play(chunk, size, error);
|
||||
}
|
||||
|
||||
static bool
|
||||
httpd_output_pause(AudioOutput *ao)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
if (httpd->LockHasClients()) {
|
||||
static const char silence[1020] = { 0 };
|
||||
return httpd_output_play(ao, silence, sizeof(silence),
|
||||
IgnoreError()) > 0;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
inline void
|
||||
HttpdOutput::SendTag(const Tag *tag)
|
||||
{
|
||||
assert(tag != nullptr);
|
||||
|
||||
if (encoder->plugin.tag != nullptr) {
|
||||
/* embed encoder tags */
|
||||
|
||||
/* flush the current stream, and end it */
|
||||
|
||||
encoder_pre_tag(encoder, IgnoreError());
|
||||
BroadcastFromEncoder();
|
||||
|
||||
/* send the tag to the encoder - which starts a new
|
||||
stream now */
|
||||
|
||||
encoder_tag(encoder, tag, IgnoreError());
|
||||
|
||||
/* the first page generated by the encoder will now be
|
||||
used as the new "header" page, which is sent to all
|
||||
new clients */
|
||||
|
||||
Page *page = ReadPage();
|
||||
if (page != nullptr) {
|
||||
if (header != nullptr)
|
||||
header->Unref();
|
||||
header = page;
|
||||
BroadcastPage(page);
|
||||
}
|
||||
} else {
|
||||
/* use Icy-Metadata */
|
||||
|
||||
if (metadata != nullptr)
|
||||
metadata->Unref();
|
||||
|
||||
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 ScopeLock protect(mutex);
|
||||
for (auto &client : clients)
|
||||
client.PushMetaData(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
httpd_output_tag(AudioOutput *ao, const Tag *tag)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
httpd->SendTag(tag);
|
||||
}
|
||||
|
||||
inline void
|
||||
HttpdOutput::CancelAllClients()
|
||||
{
|
||||
const ScopeLock protect(mutex);
|
||||
|
||||
while (!pages.empty()) {
|
||||
Page *page = pages.front();
|
||||
pages.pop();
|
||||
page->Unref();
|
||||
}
|
||||
|
||||
for (auto &client : clients)
|
||||
client.CancelQueue();
|
||||
|
||||
cond.broadcast();
|
||||
}
|
||||
|
||||
static void
|
||||
httpd_output_cancel(AudioOutput *ao)
|
||||
{
|
||||
HttpdOutput *httpd = HttpdOutput::Cast(ao);
|
||||
|
||||
BlockingCall(io_thread_get(), [httpd](){
|
||||
httpd->CancelAllClients();
|
||||
});
|
||||
}
|
||||
|
||||
const struct AudioOutputPlugin httpd_output_plugin = {
|
||||
"httpd",
|
||||
nullptr,
|
||||
httpd_output_init,
|
||||
httpd_output_finish,
|
||||
httpd_output_enable,
|
||||
httpd_output_disable,
|
||||
httpd_output_open,
|
||||
httpd_output_close,
|
||||
httpd_output_delay,
|
||||
httpd_output_tag,
|
||||
httpd_output_play,
|
||||
nullptr,
|
||||
httpd_output_cancel,
|
||||
httpd_output_pause,
|
||||
nullptr,
|
||||
};
|
||||
25
src/output/plugins/httpd/HttpdOutputPlugin.hxx
Normal file
25
src/output/plugins/httpd/HttpdOutputPlugin.hxx
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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_HTTPD_OUTPUT_PLUGIN_HXX
|
||||
#define MPD_HTTPD_OUTPUT_PLUGIN_HXX
|
||||
|
||||
extern const struct AudioOutputPlugin httpd_output_plugin;
|
||||
|
||||
#endif
|
||||
134
src/output/plugins/httpd/IcyMetaDataServer.cxx
Normal file
134
src/output/plugins/httpd/IcyMetaDataServer.cxx
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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 "config.h"
|
||||
#include "IcyMetaDataServer.hxx"
|
||||
#include "Page.hxx"
|
||||
#include "tag/Tag.hxx"
|
||||
#include "util/FormatString.hxx"
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include <string.h>
|
||||
|
||||
char*
|
||||
icy_server_metadata_header(const char *name,
|
||||
const char *genre, const char *url,
|
||||
const char *content_type, int metaint)
|
||||
{
|
||||
return FormatNew("ICY 200 OK\r\n"
|
||||
"icy-notice1:<BR>This stream requires an audio player!<BR>\r\n" /* TODO */
|
||||
"icy-notice2:MPD - The music player daemon<BR>\r\n"
|
||||
"icy-name: %s\r\n" /* TODO */
|
||||
"icy-genre: %s\r\n" /* TODO */
|
||||
"icy-url: %s\r\n" /* TODO */
|
||||
"icy-pub:1\r\n"
|
||||
"icy-metaint:%d\r\n"
|
||||
/* TODO "icy-br:%d\r\n" */
|
||||
"Content-Type: %s\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Pragma: no-cache\r\n"
|
||||
"Cache-Control: no-cache, no-store\r\n"
|
||||
"\r\n",
|
||||
name,
|
||||
genre,
|
||||
url,
|
||||
metaint,
|
||||
/* bitrate, */
|
||||
content_type);
|
||||
}
|
||||
|
||||
static char *
|
||||
icy_server_metadata_string(const char *stream_title, const char* stream_url)
|
||||
{
|
||||
gchar *icy_metadata;
|
||||
guint meta_length;
|
||||
|
||||
// The leading n is a placeholder for the length information
|
||||
icy_metadata = FormatNew("nStreamTitle='%s';"
|
||||
"StreamUrl='%s';",
|
||||
stream_title,
|
||||
stream_url);
|
||||
|
||||
meta_length = strlen(icy_metadata);
|
||||
|
||||
meta_length--; // subtract placeholder
|
||||
|
||||
meta_length = ((int)meta_length / 16) + 1;
|
||||
|
||||
icy_metadata[0] = meta_length;
|
||||
|
||||
if (meta_length > 255) {
|
||||
delete[] icy_metadata;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return icy_metadata;
|
||||
}
|
||||
|
||||
Page *
|
||||
icy_server_metadata_page(const Tag &tag, const TagType *types)
|
||||
{
|
||||
const gchar *tag_items[TAG_NUM_OF_ITEM_TYPES];
|
||||
gint last_item, item;
|
||||
guint position;
|
||||
gchar *icy_string;
|
||||
gchar stream_title[(1 + 255 - 28) * 16]; // Length + Metadata -
|
||||
// "StreamTitle='';StreamUrl='';"
|
||||
// = 4081 - 28
|
||||
stream_title[0] = '\0';
|
||||
|
||||
last_item = -1;
|
||||
|
||||
while (*types != TAG_NUM_OF_ITEM_TYPES) {
|
||||
const gchar *tag_item = tag.GetValue(*types++);
|
||||
if (tag_item)
|
||||
tag_items[++last_item] = tag_item;
|
||||
}
|
||||
|
||||
position = item = 0;
|
||||
while (position < sizeof(stream_title) && item <= last_item) {
|
||||
gint length = 0;
|
||||
|
||||
length = g_strlcpy(stream_title + position,
|
||||
tag_items[item++],
|
||||
sizeof(stream_title) - position);
|
||||
|
||||
position += length;
|
||||
|
||||
if (item <= last_item) {
|
||||
length = g_strlcpy(stream_title + position,
|
||||
" - ",
|
||||
sizeof(stream_title) - position);
|
||||
|
||||
position += length;
|
||||
}
|
||||
}
|
||||
|
||||
icy_string = icy_server_metadata_string(stream_title, "");
|
||||
|
||||
if (icy_string == nullptr)
|
||||
return nullptr;
|
||||
|
||||
Page *icy_metadata = Page::Copy(icy_string, (icy_string[0] * 16) + 1);
|
||||
|
||||
delete[] icy_string;
|
||||
|
||||
return icy_metadata;
|
||||
}
|
||||
39
src/output/plugins/httpd/IcyMetaDataServer.hxx
Normal file
39
src/output/plugins/httpd/IcyMetaDataServer.hxx
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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_ICY_META_DATA_SERVER_HXX
|
||||
#define MPD_ICY_META_DATA_SERVER_HXX
|
||||
|
||||
#include "tag/TagType.h"
|
||||
|
||||
struct Tag;
|
||||
class Page;
|
||||
|
||||
/**
|
||||
* Free the return value with delete[].
|
||||
*/
|
||||
char*
|
||||
icy_server_metadata_header(const char *name,
|
||||
const char *genre, const char *url,
|
||||
const char *content_type, int metaint);
|
||||
|
||||
Page *
|
||||
icy_server_metadata_page(const Tag &tag, const TagType *types);
|
||||
|
||||
#endif
|
||||
70
src/output/plugins/httpd/Page.cxx
Normal file
70
src/output/plugins/httpd/Page.cxx
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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 "config.h"
|
||||
#include "Page.hxx"
|
||||
#include "util/Alloc.hxx"
|
||||
|
||||
#include <new>
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
Page *
|
||||
Page::Create(size_t size)
|
||||
{
|
||||
void *p = xalloc(sizeof(Page) + size -
|
||||
sizeof(Page::data));
|
||||
return ::new(p) Page(size);
|
||||
}
|
||||
|
||||
Page *
|
||||
Page::Copy(const void *data, size_t size)
|
||||
{
|
||||
assert(data != nullptr);
|
||||
|
||||
Page *page = Create(size);
|
||||
memcpy(page->data, data, size);
|
||||
return page;
|
||||
}
|
||||
|
||||
Page *
|
||||
Page::Concat(const Page &a, const Page &b)
|
||||
{
|
||||
Page *page = Create(a.size + b.size);
|
||||
|
||||
memcpy(page->data, a.data, a.size);
|
||||
memcpy(page->data + a.size, b.data, b.size);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
bool
|
||||
Page::Unref()
|
||||
{
|
||||
bool unused = ref.Decrement();
|
||||
|
||||
if (unused) {
|
||||
this->Page::~Page();
|
||||
free(this);
|
||||
}
|
||||
|
||||
return unused;
|
||||
}
|
||||
102
src/output/plugins/httpd/Page.hxx
Normal file
102
src/output/plugins/httpd/Page.hxx
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2003-2014 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.
|
||||
*/
|
||||
|
||||
/** \file
|
||||
*
|
||||
* This is a library which manages reference counted buffers.
|
||||
*/
|
||||
|
||||
#ifndef MPD_PAGE_HXX
|
||||
#define MPD_PAGE_HXX
|
||||
|
||||
#include "util/RefCount.hxx"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* A dynamically allocated buffer which keeps track of its reference
|
||||
* count. This is useful for passing buffers around, when several
|
||||
* instances hold references to one buffer.
|
||||
*/
|
||||
class Page {
|
||||
/**
|
||||
* The number of references to this buffer. This library uses
|
||||
* atomic functions to access it, i.e. no locks are required.
|
||||
* As soon as this attribute reaches zero, the buffer is
|
||||
* freed.
|
||||
*/
|
||||
RefCount ref;
|
||||
|
||||
public:
|
||||
/**
|
||||
* The size of this buffer in bytes.
|
||||
*/
|
||||
const size_t size;
|
||||
|
||||
/**
|
||||
* Dynamic array containing the buffer data.
|
||||
*/
|
||||
unsigned char data[sizeof(long)];
|
||||
|
||||
protected:
|
||||
Page(size_t _size):size(_size) {}
|
||||
~Page() = default;
|
||||
|
||||
/**
|
||||
* Allocates a new #Page object, without filling the data
|
||||
* element.
|
||||
*/
|
||||
static Page *Create(size_t size);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Creates a new #page object, and copies data from the
|
||||
* specified buffer. It is initialized with a reference count
|
||||
* of 1.
|
||||
*
|
||||
* @param data the source buffer
|
||||
* @param size the size of the source buffer
|
||||
*/
|
||||
static Page *Copy(const void *data, size_t size);
|
||||
|
||||
/**
|
||||
* Concatenates two pages to a new page.
|
||||
*
|
||||
* @param a the first page
|
||||
* @param b the second page, which is appended
|
||||
*/
|
||||
static Page *Concat(const Page &a, const Page &b);
|
||||
|
||||
/**
|
||||
* Increases the reference counter.
|
||||
*/
|
||||
void Ref() {
|
||||
ref.Increment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decreases the reference counter. If it reaches zero, the #page is
|
||||
* freed.
|
||||
*
|
||||
* @return true if the #page has been freed
|
||||
*/
|
||||
bool Unref();
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user