aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am11
-rw-r--r--configure.ac16
-rw-r--r--doc/mpdconf.example12
-rw-r--r--doc/user.xml71
-rw-r--r--src/output/httpd_client.c523
-rw-r--r--src/output/httpd_client.h65
-rw-r--r--src/output/httpd_internal.h112
-rw-r--r--src/output/httpd_output_plugin.c393
-rw-r--r--src/output_list.c4
9 files changed, 1207 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am
index f585ca62..fa2efb7d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -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 \
diff --git a/configure.ac b/configure.ac
index 177ef21c..557ce97c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -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
diff --git a/doc/mpdconf.example b/doc/mpdconf.example
index f7816e4a..3af0567d 100644
--- a/doc/mpdconf.example
+++ b/doc/mpdconf.example
@@ -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 {
diff --git a/doc/user.xml b/doc/user.xml
index 06dc087f..6837181a 100644
--- a/doc/user.xml
+++ b/doc/user.xml
@@ -252,6 +252,77 @@ cd mpd-0.14.2</programlisting>
</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>
<para>
diff --git a/src/output/httpd_client.c b/src/output/httpd_client.c
new file mode 100644
index 00000000..fb3f0548
--- /dev/null
+++ b/src/output/httpd_client.c
@@ -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);
+}
diff --git a/src/output/httpd_client.h b/src/output/httpd_client.h
new file mode 100644
index 00000000..56a0203a
--- /dev/null
+++ b/src/output/httpd_client.h
@@ -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
diff --git a/src/output/httpd_internal.h b/src/output/httpd_internal.h
new file mode 100644
index 00000000..d19909c8
--- /dev/null
+++ b/src/output/httpd_internal.h
@@ -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
diff --git a/src/output/httpd_output_plugin.c b/src/output/httpd_output_plugin.c
new file mode 100644
index 00000000..61591af2
--- /dev/null
+++ b/src/output/httpd_output_plugin.c
@@ -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,
+};
diff --git a/src/output_list.c b/src/output_list.c
index abd66fae..f2e10758 100644
--- a/src/output_list.c
+++ b/src/output_list.c
@@ -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
@@ -65,6 +66,9 @@ const struct audio_output_plugin *audio_output_plugins[] = {
#ifdef HAVE_JACK
&jackPlugin,
#endif
+#ifdef ENABLE_HTTPD_OUTPUT
+ &httpd_output_plugin,
+#endif
NULL
};