httpd: new output plugin to replace "shout"

Let's get rid of the "shout" plugin, and the awfully complicated
icecast daemon setup!  MPD can do better if it's doing the HTTP server
stuff on its own.  This new plugin has several advantages:

- easier to set up - only one daemon, no password settings, no mount
  settings
- MPD controls the encoder and thus already knows the packet
  boundaries - icecast has to parse them
- MPD doesn't bother to encode data while nobody is listening

This implementation is very experimental (no header parsing, ignores
request URI, no icy-metadata, ...).  It should be able to suport
several encoders in parallel in the future (with different bit rates,
different codec, ...), to make MPD the perfect streaming server.  Once
MPD gets multi-player support, we can even mount several different
radio stations on one server.
This commit is contained in:
Max Kellermann 2009-03-15 03:32:34 +01:00
parent 565afefc66
commit e62580db0b
9 changed files with 1207 additions and 0 deletions

View File

@ -94,6 +94,8 @@ mpd_headers = \
src/chunk.h \
src/path.h \
src/mapper.h \
src/output/httpd_client.h \
src/output/httpd_internal.h \
src/page.h \
src/pcm_buffer.h \
src/pcm_utils.h \
@ -511,6 +513,12 @@ if HAVE_SHOUT
OUTPUT_SRC += src/output/shout_plugin.c
endif
if ENABLE_HTTPD_OUTPUT
OUTPUT_SRC += \
src/output/httpd_client.c \
src/output/httpd_output_plugin.c
endif
#
# Sparse code analysis
@ -606,6 +614,9 @@ test_run_output_SOURCES = test/run_output.c \
src/conf.c src/buffer2array.c src/utils.c src/log.c \
src/audio_parser.c \
src/timer.c \
src/fifo_buffer.c \
src/page.c \
src/socket_util.c \
src/output_init.c src/output_list.c \
$(ENCODER_SRC) \
src/mixer_api.c \

View File

@ -568,6 +568,16 @@ AC_ARG_ENABLE(shout-mp3,
[disable support for mp3 streaming through shout (default: enable)]),,
[enable_shout_mp3=yes])
AC_ARG_ENABLE(httpd-output,
AS_HELP_STRING([--enable-httpd-output],
[enables the HTTP server output (default: disable)]),,
[enable_httpd_output=no])
AM_CONDITIONAL(ENABLE_HTTPD_OUTPUT, test x$enable_httpd_output = xyes)
if test x$enable_httpd_output = xyes; then
AC_DEFINE(ENABLE_HTTPD_OUTPUT, 1, [Define to enable the HTTP server output])
fi
enable_osx=no
case "$host_os" in
darwin*)
@ -1155,6 +1165,12 @@ else
echo " SHOUTcast support .............disabled"
fi
if test x$enable_httpd_output = xyes; then
echo " HTTP daemon support ...........enabled"
else
echo " HTTP daemon support ...........disabled"
fi
echo ""
if

View File

@ -198,6 +198,18 @@ log_file "~/.mpd/log"
# timeout "2" # optional
#}
#
# An example of a httpd output (built-in HTTP streaming server):
#
#audio_output {
# type "httpd"
# name "My HTTP Stream"
# encoder "ogg" # optional
# port "8000"
# quality "5.0"
# bitrate "128"
# format "44100:16:1"
#}
#
# An example of a pulseaudio output (streaming to a remote pulseaudio server)
#
#audio_output {

View File

@ -251,6 +251,77 @@ cd mpd-0.14.2</programlisting>
</para>
</section>
<section>
<title><varname>httpd</varname></title>
<para>
The <varname>httpd</varname> plugin creates a HTTP server,
similar to ShoutCast / IceCast. HTTP streaming clients like
<filename>mplayer</filename> can connect to it.
</para>
<para>
You must configure either <varname>quality</varname> or
<varname>bitrate</varname>. It is highly recommended to
configure a fixed <varname>format</varname>, because a
stream cannot switch its audio format on-the-fly when the
song changes.
</para>
<informaltable>
<tgroup cols="2">
<thead>
<row>
<entry>Setting</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry>
<varname>port</varname>
<parameter>P</parameter>
</entry>
<entry>
Binds the HTTP server to the specified port (on all
interfaces).
</entry>
</row>
<row>
<entry>
<varname>encoder</varname>
<parameter>NAME</parameter>
</entry>
<entry>
Chooses an encoder plugin,
e.g. <parameter>vorbis</parameter>.
</entry>
</row>
<row>
<entry>
<varname>quality</varname>
<parameter>Q</parameter>
</entry>
<entry>
Configures the encoder quality (for VBR) in the
range -1 .. 10.
</entry>
</row>
<row>
<entry>
<varname>bitrate</varname>
<parameter>BR</parameter>
</entry>
<entry>
Sets a constant encoder bit rate, in kilobit per
second.
</entry>
</row>
</tbody>
</tgroup>
</informaltable>
</section>
<section>
<title><varname>null</varname></title>

523
src/output/httpd_client.c Normal file
View File

@ -0,0 +1,523 @@
/*
* Copyright (C) 2003-2009 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 "httpd_client.h"
#include "httpd_internal.h"
#include "fifo_buffer.h"
#include "page.h"
#include <stdbool.h>
#include <assert.h>
#include <string.h>
struct httpd_client {
/**
* The httpd output object this client is connected to.
*/
struct httpd_output *httpd;
/**
* The TCP socket.
*/
GIOChannel *channel;
/**
* The GLib main loop source id for reading from the socket,
* and to detect errors.
*/
guint read_source_id;
/**
* The GLib main loop source id for writing to the socket. If
* 0, then there is no event source currently (because there
* are no queued pages).
*/
guint write_source_id;
/**
* For buffered reading. This pointer is only valid while the
* HTTP request is read.
*/
struct fifo_buffer *input;
/**
* 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.
*/
GQueue *pages;
/**
* The #page which is currently being sent to the client.
*/
struct page *current_page;
/**
* The amount of bytes which were already sent from
* #current_page.
*/
size_t current_position;
};
static void
httpd_client_unref_page(gpointer data, G_GNUC_UNUSED gpointer user_data)
{
struct page *page = data;
page_unref(page);
}
void
httpd_client_free(struct httpd_client *client)
{
if (client->state == RESPONSE) {
if (client->write_source_id != 0)
g_source_remove(client->write_source_id);
if (client->current_page != NULL)
page_unref(client->current_page);
g_queue_foreach(client->pages, httpd_client_unref_page, NULL);
g_queue_free(client->pages);
} else
fifo_buffer_free(client->input);
g_source_remove(client->read_source_id);
g_io_channel_unref(client->channel);
g_free(client);
}
/**
* Frees the client and removes it from the server's client list.
*/
static void
httpd_client_close(struct httpd_client *client)
{
httpd_output_remove_client(client->httpd, client);
httpd_client_free(client);
}
/**
* Switch the client to the "RESPONSE" state.
*/
static void
httpd_client_begin_response(struct httpd_client *client)
{
client->state = RESPONSE;
client->write_source_id = 0;
client->pages = g_queue_new();
client->current_page = NULL;
httpd_output_send_header(client->httpd, client);
}
/**
* Handle a line of the HTTP request.
*/
static bool
httpd_client_handle_line(struct httpd_client *client, const char *line)
{
assert(client->state != RESPONSE);
if (client->state == REQUEST) {
if (strncmp(line, "GET /", 5) != 0) {
/* only GET is supported */
g_warning("malformed request line from client");
return false;
}
line = strchr(line + 5, ' ');
if (line == NULL || strncmp(line + 1, "HTTP/", 5) != 0) {
/* HTTP/0.9 without request headers */
httpd_client_begin_response(client);
return true;
}
/* after the request line, request headers follow */
client->state = HEADERS;
return true;
} else {
if (*line == 0) {
/* empty line: request is finished */
httpd_client_begin_response(client);
return true;
}
/* expect more request headers */
return true;
}
}
/**
* Check if a complete line of input is present in the input buffer,
* and duplicates it. It is removed from the input buffer. The
* return value has to be freed with g_free().
*/
static char *
httpd_client_read_line(struct httpd_client *client)
{
const char *p, *newline;
size_t length;
char *line;
p = fifo_buffer_read(client->input, &length);
if (p == NULL)
/* empty input buffer */
return NULL;
newline = memchr(p, '\n', length);
if (newline == NULL)
/* incomplete line */
return NULL;
line = g_strndup(p, newline - p);
fifo_buffer_consume(client->input, newline - p + 1);
/* remove trailing whitespace (e.g. '\r') */
return g_strchomp(line);
}
/**
* Sends the status line and response headers to the client.
*/
static bool
httpd_client_send_response(struct httpd_client *client)
{
char buffer[1024];
GError *error = NULL;
GIOStatus status;
gsize bytes_written;
assert(client->state == RESPONSE);
g_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",
client->httpd->content_type);
status = g_io_channel_write_chars(client->channel,
buffer, strlen(buffer),
&bytes_written, &error);
switch (status) {
case G_IO_STATUS_NORMAL:
case G_IO_STATUS_AGAIN:
return true;
case G_IO_STATUS_EOF:
/* client has disconnected */
httpd_client_close(client);
return false;
case G_IO_STATUS_ERROR:
/* I/O error */
g_warning("failed to write to client: %s", error->message);
g_error_free(error);
httpd_client_close(client);
return false;
}
/* unreachable */
httpd_client_close(client);
return false;
}
/**
* Data has been received from the client and it is appended to the
* input buffer.
*/
static bool
httpd_client_received(struct httpd_client *client)
{
char *line;
bool success;
while ((line = httpd_client_read_line(client)) != NULL) {
success = httpd_client_handle_line(client, line);
g_free(line);
if (!success)
return false;
if (client->state == RESPONSE) {
if (!fifo_buffer_is_empty(client->input)) {
g_warning("unexpected input from client");
return false;
}
fifo_buffer_free(client->input);
return httpd_client_send_response(client);
}
}
return true;
}
static bool
httpd_client_read(struct httpd_client *client)
{
char *p;
size_t max_length;
GError *error = NULL;
GIOStatus status;
gsize bytes_read;
if (client->state == RESPONSE) {
/* the client has already sent the request, and he
must not send more */
g_warning("unexpected input from client");
return false;
}
p = fifo_buffer_write(client->input, &max_length);
if (p == NULL) {
g_warning("buffer overflow");
return false;
}
status = g_io_channel_read_chars(client->channel, p, max_length,
&bytes_read, &error);
switch (status) {
case G_IO_STATUS_NORMAL:
fifo_buffer_append(client->input, bytes_read);
return httpd_client_received(client);
case G_IO_STATUS_AGAIN:
/* try again later, after select() */
return true;
case G_IO_STATUS_EOF:
/* peer disconnected */
return false;
case G_IO_STATUS_ERROR:
/* I/O error */
g_warning("failed to read from client: %s",
error->message);
g_error_free(error);
return false;
}
/* unreachable */
return false;
}
static gboolean
httpd_client_in_event(G_GNUC_UNUSED GIOChannel *source, GIOCondition condition,
gpointer data)
{
struct httpd_client *client = data;
struct httpd_output *httpd = client->httpd;
bool ret;
g_mutex_lock(httpd->mutex);
if (condition == G_IO_IN && httpd_client_read(client)) {
ret = true;
} else {
httpd_client_close(client);
ret = false;
}
g_mutex_unlock(httpd->mutex);
return ret;
}
struct httpd_client *
httpd_client_new(struct httpd_output *httpd, int fd)
{
struct httpd_client *client = g_new(struct httpd_client, 1);
client->httpd = httpd;
#ifndef G_OS_WIN32
client->channel = g_io_channel_unix_new(fd);
#else
client->channel = g_io_channel_win32_new_socket(fd);
#endif
/* GLib is responsible for closing the file descriptor */
g_io_channel_set_close_on_unref(client->channel, true);
/* NULL encoding means the stream is binary safe */
g_io_channel_set_encoding(client->channel, NULL, NULL);
/* we prefer to do buffering */
g_io_channel_set_buffered(client->channel, false);
client->read_source_id = g_io_add_watch(client->channel,
G_IO_IN|G_IO_ERR|G_IO_HUP,
httpd_client_in_event, client);
client->input = fifo_buffer_new(4096);
client->state = REQUEST;
return client;
}
static void
httpd_client_add_page_size(gpointer data, gpointer user_data)
{
struct page *page = data;
size_t *size = user_data;
*size += page->size;
}
size_t
httpd_client_queue_size(const struct httpd_client *client)
{
size_t size = 0;
if (client->state != RESPONSE)
return 0;
g_queue_foreach(client->pages, httpd_client_add_page_size, &size);
return size;
}
void
httpd_client_cancel(const struct httpd_client *client)
{
if (client->state != RESPONSE)
return;
g_queue_foreach(client->pages, httpd_client_unref_page, NULL);
}
static GIOStatus
write_page_to_channel(GIOChannel *channel,
const struct page *page, size_t position,
gsize *bytes_written_r, GError **error)
{
assert(channel != NULL);
assert(page != NULL);
assert(position < page->size);
return g_io_channel_write_chars(channel,
(const gchar*)page->data + position,
page->size - position,
bytes_written_r, error);
}
static gboolean
httpd_client_out_event(GIOChannel *source,
G_GNUC_UNUSED GIOCondition condition, gpointer data)
{
struct httpd_client *client = data;
struct httpd_output *httpd = client->httpd;
GError *error = NULL;
GIOStatus status;
gsize bytes_written;
g_mutex_lock(httpd->mutex);
assert(condition == G_IO_OUT);
assert(client->state == RESPONSE);
if (client->current_page == NULL) {
client->current_page = g_queue_pop_head(client->pages);
client->current_position = 0;
}
status = write_page_to_channel(source, client->current_page,
client->current_position,
&bytes_written, &error);
switch (status) {
case G_IO_STATUS_NORMAL:
client->current_position += bytes_written;
assert(client->current_position <= client->current_page->size);
if (client->current_position >= client->current_page->size) {
page_unref(client->current_page);
client->current_page = NULL;
if (g_queue_is_empty(client->pages)) {
/* all pages are sent: remove the
event source */
client->write_source_id = 0;
g_mutex_unlock(httpd->mutex);
return false;
}
}
g_mutex_unlock(httpd->mutex);
return true;
case G_IO_STATUS_AGAIN:
g_mutex_unlock(httpd->mutex);
return true;
case G_IO_STATUS_EOF:
/* client has disconnected */
httpd_client_close(client);
g_mutex_unlock(httpd->mutex);
return false;
case G_IO_STATUS_ERROR:
/* I/O error */
g_warning("failed to write to client: %s", error->message);
g_error_free(error);
httpd_client_close(client);
g_mutex_unlock(httpd->mutex);
return false;
}
/* unreachable */
httpd_client_close(client);
g_mutex_unlock(httpd->mutex);
return false;
}
void
httpd_client_send(struct httpd_client *client, struct page *page)
{
if (client->state != RESPONSE)
/* the client is still writing the HTTP request */
return;
page_ref(page);
g_queue_push_tail(client->pages, page);
if (client->write_source_id == 0)
client->write_source_id =
g_io_add_watch(client->channel, G_IO_OUT,
httpd_client_out_event, client);
}

65
src/output/httpd_client.h Normal file
View File

@ -0,0 +1,65 @@
/*
* Copyright (C) 2003-2009 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_H
#define MPD_OUTPUT_HTTPD_CLIENT_H
#include <glib.h>
#include <stdbool.h>
struct httpd_client;
struct httpd_output;
struct page;
/**
* Creates a new #httpd_client object
*
* @param httpd the HTTP output device
* @param fd the socket file descriptor
*/
struct httpd_client *
httpd_client_new(struct httpd_output *httpd, int fd);
/**
* Frees memory and resources allocated by the #httpd_client object.
* This does not remove it from the #httpd_output object.
*/
void
httpd_client_free(struct httpd_client *client);
/**
* Returns the total size of this client's page queue.
*/
size_t
httpd_client_queue_size(const struct httpd_client *client);
/**
* Clears the page queue.
*/
void
httpd_client_cancel(const struct httpd_client *client);
/**
* Appends a page to the client's queue.
*/
void
httpd_client_send(struct httpd_client *client, struct page *page);
#endif

112
src/output/httpd_internal.h Normal file
View File

@ -0,0 +1,112 @@
/*
* Copyright (C) 2003-2009 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 "timer.h"
#include <glib.h>
#include <sys/socket.h>
struct httpd_client;
struct httpd_output {
/**
* The configured encoder plugin.
*/
struct encoder *encoder;
/**
* The MIME type produced by the #encoder.
*/
const char *content_type;
/**
* The configured address of the listener socket.
*/
struct sockaddr_storage address;
/**
* The size of #address.
*/
socklen_t address_size;
/**
* This mutex protects the listener socket and the client
* list.
*/
GMutex *mutex;
/**
* A #Timer object to synchronize this output with the
* wallclock.
*/
Timer *timer;
/**
* The listener socket.
*/
int fd;
/**
* A GLib main loop source id for the listener socket.
*/
guint source_id;
/**
* The header page, which is sent to every client on connect.
*/
struct page *header;
/**
* A linked list containing all clients which are currently
* connected.
*/
GList *clients;
/**
* A temporary buffer for the httpd_output_read_page()
* function.
*/
char buffer[32768];
};
/**
* Removes a client from the httpd_output.clients linked list.
*/
void
httpd_output_remove_client(struct httpd_output *httpd,
struct httpd_client *client);
/**
* Sends the encoder header to the client. This is called right after
* the response headers have been sent.
*/
void
httpd_output_send_header(struct httpd_output *httpd,
struct httpd_client *client);
#endif

View File

@ -0,0 +1,393 @@
/*
* Copyright (C) 2003-2009 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 "httpd_internal.h"
#include "httpd_client.h"
#include "output_api.h"
#include "encoder_plugin.h"
#include "encoder_list.h"
#include "socket_util.h"
#include "page.h"
#include <assert.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "httpd_output"
/**
* The quark used for GError.domain.
*/
static inline GQuark
httpd_output_quark(void)
{
return g_quark_from_static_string("httpd_output");
}
static void *
httpd_output_init(G_GNUC_UNUSED const struct audio_format *audio_format,
const struct config_param *param,
GError **error)
{
struct httpd_output *httpd = g_new(struct httpd_output, 1);
const char *encoder_name;
const struct encoder_plugin *encoder_plugin;
guint port;
struct sockaddr_in *sin;
/* read configuration */
port = config_get_block_unsigned(param, "port", 8000);
encoder_name = config_get_block_string(param, "encoder", "vorbis");
encoder_plugin = encoder_plugin_get(encoder_name);
if (encoder_plugin_get == NULL) {
g_set_error(error, httpd_output_quark(), 0,
"No such encoder: %s", encoder_name);
return NULL;
}
if (strcmp(encoder_name, "vorbis") == 0)
httpd->content_type = "application/x-ogg";
else if (strcmp(encoder_name, "lame") == 0)
httpd->content_type = "audio/mpeg";
else
httpd->content_type = "application/octet-stream";
/* initialize listen address */
sin = (struct sockaddr_in *)&httpd->address;
memset(sin, 0, sizeof(sin));
sin->sin_port = htons(port);
sin->sin_family = AF_INET;
sin->sin_addr.s_addr = INADDR_ANY;
httpd->address_size = sizeof(*sin);
/* initialize encoder */
httpd->encoder = encoder_init(encoder_plugin, param, error);
if (httpd->encoder == NULL)
return NULL;
httpd->mutex = g_mutex_new();
return httpd;
}
static void
httpd_output_finish(void *data)
{
struct httpd_output *httpd = data;
encoder_finish(httpd->encoder);
g_mutex_free(httpd->mutex);
g_free(httpd);
}
/**
* Creates a new #httpd_client object and adds it into the
* httpd_output.clients linked list.
*/
static void
httpd_client_add(struct httpd_output *httpd, int fd)
{
struct httpd_client *client = httpd_client_new(httpd, fd);
httpd->clients = g_list_prepend(httpd->clients, client);
}
static gboolean
httpd_listen_in_event(G_GNUC_UNUSED GIOChannel *source,
G_GNUC_UNUSED GIOCondition condition,
gpointer data)
{
struct httpd_output *httpd = data;
int fd;
struct sockaddr_storage sa;
socklen_t sa_length = sizeof(sa);
g_mutex_lock(httpd->mutex);
/* the listener socket has become readable - a client has
connected */
fd = accept(httpd->fd, (struct sockaddr*)&sa, &sa_length);
if (fd >= 0)
httpd_client_add(httpd, fd);
else if (fd < 0 && errno != EINTR)
g_warning("accept() failed: %s", g_strerror(errno));
g_mutex_unlock(httpd->mutex);
return true;
}
/**
* Reads data from the encoder (as much as available) and returns it
* as a new #page object.
*/
static struct page *
httpd_output_read_page(struct httpd_output *httpd)
{
size_t size = 0, nbytes;
do {
nbytes = encoder_read(httpd->encoder, httpd->buffer + size,
sizeof(httpd->buffer) - size);
if (nbytes == 0)
break;
size += nbytes;
} while (size < sizeof(httpd->buffer));
if (size == 0)
return NULL;
return page_new_copy(httpd->buffer, size);
}
static bool
httpd_output_encoder_open(struct httpd_output *httpd,
struct audio_format *audio_format,
GError **error)
{
bool success;
success = encoder_open(httpd->encoder, audio_format, error);
if (!success)
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 */
httpd->header = httpd_output_read_page(httpd);
return true;
}
static bool
httpd_output_open(void *data, struct audio_format *audio_format,
GError **error)
{
struct httpd_output *httpd = data;
bool success;
GIOChannel *channel;
g_mutex_lock(httpd->mutex);
/* create and set up listener socket */
httpd->fd = socket_bind_listen(PF_INET, SOCK_STREAM, 0,
(struct sockaddr *)&httpd->address,
httpd->address_size,
16, error);
if (httpd->fd < 0) {
g_mutex_unlock(httpd->mutex);
return false;
}
channel = g_io_channel_unix_new(httpd->fd);
httpd->source_id = g_io_add_watch(channel, G_IO_IN,
httpd_listen_in_event, httpd);
g_io_channel_unref(channel);
/* open the encoder */
success = httpd_output_encoder_open(httpd, audio_format, error);
if (!success) {
g_source_remove(httpd->source_id);
close(httpd->fd);
g_mutex_unlock(httpd->mutex);
return false;
}
/* initialize other attributes */
httpd->clients = NULL;
httpd->timer = timer_new(audio_format);
g_mutex_unlock(httpd->mutex);
return true;
}
static void
httpd_client_delete(gpointer data, G_GNUC_UNUSED gpointer user_data)
{
struct httpd_client *client = data;
httpd_client_free(client);
}
static void httpd_output_close(void *data)
{
struct httpd_output *httpd = data;
g_mutex_lock(httpd->mutex);
timer_free(httpd->timer);
g_list_foreach(httpd->clients, httpd_client_delete, NULL);
g_list_free(httpd->clients);
if (httpd->header != NULL)
page_unref(httpd->header);
encoder_close(httpd->encoder);
g_source_remove(httpd->source_id);
close(httpd->fd);
g_mutex_unlock(httpd->mutex);
}
void
httpd_output_remove_client(struct httpd_output *httpd,
struct httpd_client *client)
{
assert(httpd != NULL);
assert(client != NULL);
httpd->clients = g_list_remove(httpd->clients, client);
}
void
httpd_output_send_header(struct httpd_output *httpd,
struct httpd_client *client)
{
if (httpd->header != NULL)
httpd_client_send(client, httpd->header);
}
static void
httpd_client_check_queue(gpointer data, G_GNUC_UNUSED gpointer user_data)
{
struct httpd_client *client = data;
if (httpd_client_queue_size(client) > 256 * 1024) {
g_debug("client is too slow, flushing its queue");
httpd_client_cancel(client);
}
}
static void
httpd_client_send_page(gpointer data, gpointer user_data)
{
struct httpd_client *client = data;
struct page *page = user_data;
httpd_client_send(client, page);
}
static bool
httpd_output_encode_and_play(struct httpd_output *httpd,
const void *chunk, size_t size, GError **error)
{
bool success;
struct page *page;
success = encoder_write(httpd->encoder, chunk, size, error);
if (!success)
return false;
g_mutex_lock(httpd->mutex);
g_list_foreach(httpd->clients, httpd_client_check_queue, page);
g_mutex_unlock(httpd->mutex);
while ((page = httpd_output_read_page(httpd)) != NULL) {
g_mutex_lock(httpd->mutex);
g_list_foreach(httpd->clients,
httpd_client_send_page, page);
g_mutex_unlock(httpd->mutex);
page_unref(page);
}
return true;
}
static size_t
httpd_output_play(void *data, const void *chunk, size_t size, GError **error)
{
struct httpd_output *httpd = data;
bool has_clients;
g_mutex_lock(httpd->mutex);
has_clients = httpd->clients != NULL;
g_mutex_unlock(httpd->mutex);
if (has_clients) {
bool success;
success = httpd_output_encode_and_play(httpd, chunk, size,
error);
if (!success)
return 0;
}
if (!httpd->timer->started)
timer_start(httpd->timer);
else
timer_sync(httpd->timer);
timer_add(httpd->timer, size);
return size;
}
static void
httpd_output_tag(void *data, const struct tag *tag)
{
struct httpd_output *httpd = data;
/* XXX add suport for icy-metadata */
encoder_tag(httpd->encoder, tag, NULL);
}
static void
httpd_client_cancel_callback(gpointer data, G_GNUC_UNUSED gpointer user_data)
{
struct httpd_client *client = data;
httpd_client_cancel(client);
}
static void
httpd_output_cancel(void *data)
{
struct httpd_output *httpd = data;
g_mutex_lock(httpd->mutex);
g_list_foreach(httpd->clients, httpd_client_cancel_callback, NULL);
g_mutex_unlock(httpd->mutex);
}
const struct audio_output_plugin httpd_output_plugin = {
.name = "httpd",
.init = httpd_output_init,
.finish = httpd_output_finish,
.open = httpd_output_open,
.close = httpd_output_close,
.send_tag = httpd_output_tag,
.play = httpd_output_play,
.cancel = httpd_output_cancel,
};

View File

@ -32,6 +32,7 @@ extern const struct audio_output_plugin osxPlugin;
extern const struct audio_output_plugin pulse_plugin;
extern const struct audio_output_plugin mvp_output_plugin;
extern const struct audio_output_plugin jackPlugin;
extern const struct audio_output_plugin httpd_output_plugin;
const struct audio_output_plugin *audio_output_plugins[] = {
#ifdef HAVE_SHOUT
@ -64,6 +65,9 @@ const struct audio_output_plugin *audio_output_plugins[] = {
#endif
#ifdef HAVE_JACK
&jackPlugin,
#endif
#ifdef ENABLE_HTTPD_OUTPUT
&httpd_output_plugin,
#endif
NULL
};