diff options
author | Max Kellermann <max@duempel.org> | 2009-03-15 03:32:34 +0100 |
---|---|---|
committer | Max Kellermann <max@duempel.org> | 2009-03-15 03:32:34 +0100 |
commit | e62580db0bab0650f088f9f45c66657d18dfb679 (patch) | |
tree | 19c0deec7b75ea7fe2513c674c7de3fe2647fa7c | |
parent | 565afefc66b7753ed27f6b7febaf32667c38c44a (diff) | |
download | mpd-e62580db0bab0650f088f9f45c66657d18dfb679.tar.gz mpd-e62580db0bab0650f088f9f45c66657d18dfb679.tar.xz mpd-e62580db0bab0650f088f9f45c66657d18dfb679.zip |
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.
Diffstat (limited to '')
-rw-r--r-- | Makefile.am | 11 | ||||
-rw-r--r-- | configure.ac | 16 | ||||
-rw-r--r-- | doc/mpdconf.example | 12 | ||||
-rw-r--r-- | doc/user.xml | 71 | ||||
-rw-r--r-- | src/output/httpd_client.c | 523 | ||||
-rw-r--r-- | src/output/httpd_client.h | 65 | ||||
-rw-r--r-- | src/output/httpd_internal.h | 112 | ||||
-rw-r--r-- | src/output/httpd_output_plugin.c | 393 | ||||
-rw-r--r-- | src/output_list.c | 4 |
9 files changed, 1207 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am index f585ca62e..fa2efb7d6 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 177ef21c4..557ce97cb 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 f7816e4ae..3af0567d6 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 06dc087f0..6837181a9 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 000000000..fb3f0548a --- /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 000000000..56a0203a3 --- /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 000000000..d19909c83 --- /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 000000000..61591af2c --- /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 abd66fae3..f2e107584 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 }; |