diff options
-rw-r--r-- | ChangeLog | 37 | ||||
-rw-r--r-- | configure.ac | 4 | ||||
-rw-r--r-- | docs/plugins/Makefile.am | 1 | ||||
-rw-r--r-- | docs/plugins/gst-plugins-bad-plugins-docs.sgml | 1 | ||||
-rw-r--r-- | docs/plugins/gst-plugins-bad-plugins-sections.txt | 14 | ||||
-rw-r--r-- | ext/soup/gstsouphttpsrc.c | 735 | ||||
-rw-r--r-- | ext/soup/gstsouphttpsrc.h | 11 | ||||
-rw-r--r-- | tests/check/Makefile.am | 11 | ||||
-rw-r--r-- | tests/check/elements/.gitignore | 1 | ||||
-rw-r--r-- | tests/check/elements/souphttpsrc.c | 408 |
10 files changed, 957 insertions, 266 deletions
@@ -1,3 +1,40 @@ +2008-01-30 Sebastian Dröge <slomo@circular-chaos.org> + + Patch by: Wouter Cloetens <wouter at mind dot be> + + * docs/plugins/Makefile.am: + * docs/plugins/gst-plugins-bad-plugins-docs.sgml: + * docs/plugins/gst-plugins-bad-plugins-sections.txt: + Add souphttpsrc to the docs. + + * configure.ac: + * ext/soup/gstsouphttpsrc.c: (gst_souphttp_src_class_init), + (gst_souphttp_src_init), (gst_souphttp_src_dispose), + (gst_souphttp_src_set_property), (gst_souphttp_src_get_property), + (gst_souphttp_src_cancel_message), + (gst_souphttp_src_queue_message), + (gst_souphttp_src_add_range_header), + (gst_souphttp_src_session_unpause_message), + (gst_souphttp_src_session_pause_message), + (gst_souphttp_src_session_close), + (gst_souphttp_src_got_headers_cb), (gst_souphttp_src_got_body_cb), + (gst_souphttp_src_finished_cb), (gst_souphttp_src_got_chunk_cb), + (gst_souphttp_src_response_cb), (gst_souphttp_src_parse_status), + (gst_souphttp_src_create), (gst_souphttp_src_start), + (gst_souphttp_src_stop), (gst_souphttp_src_unlock), + (gst_souphttp_src_unlock_stop), (gst_souphttp_src_get_size), + (gst_souphttp_src_is_seekable), (gst_souphttp_src_do_seek), + (gst_souphttp_src_set_location), (gst_souphttp_src_set_proxy), + (plugin_init): + * ext/soup/gstsouphttpsrc.h: + Add support for libsoup2.4 and require it. Also implement redirection + and manual proxy specification. Fixes bug #510708. + + * tests/check/Makefile.am: + * tests/check/elements/.cvsignore: + * tests/check/elements/souphttpsrc.c: + Add unit test for souphttpsrc. + 2008-01-30 Edgard Lima <edgard.lima@indt.org.br> * ext/Makefile.am: diff --git a/configure.ac b/configure.ac index 2876ecd1..20b96a6f 100644 --- a/configure.ac +++ b/configure.ac @@ -822,8 +822,8 @@ AG_GST_CHECK_FEATURE(NEON, [neon http client plugins], neonhttpsrc, [ dnl *** soup *** translit(dnm, m, l) AM_CONDITIONAL(USE_SOUP, true) -AG_GST_CHECK_FEATURE(SOUP, [soup http client plugins], souphttpsrc, [ - PKG_CHECK_MODULES(SOUP, libsoup-2.2 >= 2.2.104, HAVE_SOUP="yes", [ +AG_GST_CHECK_FEATURE(SOUP, [soup http client plugin (2.4)], souphttpsrc, [ + PKG_CHECK_MODULES(SOUP, libsoup-2.4, HAVE_SOUP="yes", [ HAVE_SOUP="no" AC_MSG_RESULT(no) ]) diff --git a/docs/plugins/Makefile.am b/docs/plugins/Makefile.am index 29b3300b..2f49f2af 100644 --- a/docs/plugins/Makefile.am +++ b/docs/plugins/Makefile.am @@ -103,6 +103,7 @@ EXTRA_HFILES = \ $(top_srcdir)/ext/gio/gstgiosrc.h \ $(top_srcdir)/ext/gio/gstgiostreamsink.h \ $(top_srcdir)/ext/gio/gstgiostreamsrc.h \ + $(top_srcdir)/ext/soup/gstsouphttpsrc.h \ $(top_srcdir)/ext/ivorbis/vorbisdec.h \ $(top_srcdir)/ext/jack/gstjackaudiosink.h \ $(top_srcdir)/ext/musicbrainz/gsttrm.h \ diff --git a/docs/plugins/gst-plugins-bad-plugins-docs.sgml b/docs/plugins/gst-plugins-bad-plugins-docs.sgml index d2f00cca..3d0e50e5 100644 --- a/docs/plugins/gst-plugins-bad-plugins-docs.sgml +++ b/docs/plugins/gst-plugins-bad-plugins-docs.sgml @@ -53,6 +53,7 @@ <xi:include href="xml/element-sdlaudiosink.xml" /> <xi:include href="xml/element-sdlvideosink.xml" /> <xi:include href="xml/element-sdpdemux.xml" /> + <xi:include href="xml/element-souphttpsrc.xml" /> <xi:include href="xml/element-spectrum.xml" /> <xi:include href="xml/element-speed.xml" /> <xi:include href="xml/element-speexresample.xml" /> diff --git a/docs/plugins/gst-plugins-bad-plugins-sections.txt b/docs/plugins/gst-plugins-bad-plugins-sections.txt index e7ca6f3b..295ea113 100644 --- a/docs/plugins/gst-plugins-bad-plugins-sections.txt +++ b/docs/plugins/gst-plugins-bad-plugins-sections.txt @@ -267,6 +267,20 @@ gst_gio_stream_src_get_type </SECTION> <SECTION> +<FILE>element-souphttpsrc</FILE> +<TITLE>souphttpsrc</TITLE> +GstSouphttpSrc +<SUBSECTION Standard> +GstSouphttpSrcClass +GST_SOUPHTTP_SRC +GST_SOUPHTTP_SRC_CLASS +GST_IS_SOUPHTTP_SRC +GST_IS_SOUPHTTP_SRC_CLASS +GST_TYPE_SOUPHTTP_SRC +gst_souphttp_src_get_type +</SECTION> + +<SECTION> <FILE>element-bpwsinc</FILE> <TITLE>bpwsinc</TITLE> GstBPWSinc diff --git a/ext/soup/gstsouphttpsrc.c b/ext/soup/gstsouphttpsrc.c index 8e850519..dff897c6 100644 --- a/ext/soup/gstsouphttpsrc.c +++ b/ext/soup/gstsouphttpsrc.c @@ -1,5 +1,5 @@ /* GStreamer - * Copyright (C) <2007> Wouter Cloetens <wouter@mind.be> + * Copyright (C) 2007-2008 Wouter Cloetens <wouter@mind.be> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public @@ -12,6 +12,72 @@ * Library General Public License for more */ +/** + * SECTION:element-souphttpsrc + * @short_description: Read from an HTTP/HTTPS/WebDAV/Icecast/Shoutcast + * location. + * + * <refsect2> + * <para> + * This plugin reads data from a remote location specified by a URI. + * Supported protocols are 'http', 'https', 'dav', or 'davs'. + * </para> + * <para> + * In case the element-souphttpsrc::iradio-mode property is set and the + * location is a http resource, souphttpsrc will send special Icecast HTTP + * headers to the server to request additional Icecast meta-information. If + * the server is not an Icecast server, it will behave as if the + * element-souphttpsrc::iradio-mode property were not set. If it is, + * souphttpsrc will output data with a media type of application/x-icy, + * in which case you will need to use the #ICYDemux element as follow-up + * element to extract the Icecast metadata and to determine the underlying + * media type. + * </para> + * <para> + * Example pipeline: + * <programlisting> + * gst-launch -v souphttpsrc location=https://some.server.org/index.html + * ! filesink location=/home/joe/server.html + * </programlisting> + * The above pipeline reads a web page from a server using the HTTPS protocol + * and writes it to a local file. + * </para> + * <para> + * Another example pipeline: + * <programlisting> + * gst-launch -v souphttpsrc user-agent="FooPlayer 0.99 beta" + * automatic-redirect=false proxy=http://proxy.intranet.local:8080 + * location=http://music.foobar.com/demo.mp3 ! mad ! audioconvert + * ! audioresample ! alsasink + * </programlisting> + * The above pipeline will read and decode and play an mp3 file from a + * web server using the HTTP protocol. If the server sends redirects, + * the request fails instead of following the redirect. The specified + * HTTP proxy server is used. The User-Agent HTTP request header + * is set to a custom string instead of "GStreamer souphttpsrc." + * </para> + * <para> + * Yet another example pipeline: + * <programlisting> + * gst-launch -v souphttpsrc location=http://10.11.12.13/mjpeg + * do-timestamp=true ! multipartdemux + * ! image/jpeg,width=640,height=480 ! matroskamux + * ! filesink location=mjpeg.mkv + * </programlisting> + * The above pipeline reads a motion JPEG stream from an IP camera + * using the HTTP protocol, encoded as mime/multipart image/jpeg + * parts, and writes a Matroska motion JPEG file. The width and + * height properties are set in the caps to provide the Matroska + * multiplexer with the information to set this in the header. + * Timestamps are set on the buffers as they arrive from the camera. + * These are used by the mime/multipart demultiplexer to emit timestamps + * on the JPEG-encoded video frame buffers. This allows the Matroska + * multiplexer to timestamp the frames in the resulting file. + * </para> + * </refsect2> + * + */ + #ifdef HAVE_CONFIG_H #include "config.h" #endif @@ -43,6 +109,8 @@ enum PROP_0, PROP_LOCATION, PROP_USER_AGENT, + PROP_AUTOMATIC_REDIRECT, + PROP_PROXY, PROP_IRADIO_MODE, PROP_IRADIO_NAME, PROP_IRADIO_GENRE, @@ -75,18 +143,32 @@ static gboolean gst_souphttp_src_unlock_stop (GstBaseSrc * bsrc); static gboolean gst_souphttp_src_set_location (GstSouphttpSrc * src, const gchar * uri); -static gboolean soup_add_range_header (GstSouphttpSrc * src, guint64 offset); - -static void soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src); -static void soup_finished (SoupMessage * msg, GstSouphttpSrc * src); -static void soup_got_body (SoupMessage * msg, GstSouphttpSrc * src); -static void soup_got_chunk (SoupMessage * msg, GstSouphttpSrc * src); -static void soup_response (SoupMessage * msg, gpointer user_data); -static void soup_parse_status (SoupMessage * msg, GstSouphttpSrc * src); -static void soup_session_close (GstSouphttpSrc * src); +static gboolean gst_souphttp_src_set_proxy (GstSouphttpSrc * src, + const gchar * uri); static char *gst_souphttp_src_unicodify (const char *str); +static void gst_souphttp_src_cancel_message (GstSouphttpSrc * src); +static void gst_souphttp_src_queue_message (GstSouphttpSrc * src); +static gboolean gst_souphttp_src_add_range_header (GstSouphttpSrc * src, + guint64 offset); +static void gst_souphttp_src_session_unpause_message (GstSouphttpSrc * src); +static void gst_souphttp_src_session_pause_message (GstSouphttpSrc * src); +static void gst_souphttp_src_session_close (GstSouphttpSrc * src); +static void gst_souphttp_src_parse_status (SoupMessage * msg, + GstSouphttpSrc * src); +static void gst_souphttp_src_got_chunk_cb (SoupMessage * msg, + SoupBuffer * chunk, GstSouphttpSrc * src); +static void gst_souphttp_src_response_cb (SoupSession * session, + SoupMessage * msg, GstSouphttpSrc * src); +static void gst_souphttp_src_got_headers_cb (SoupMessage * msg, + GstSouphttpSrc * src); +static void gst_souphttp_src_got_body_cb (SoupMessage * msg, + GstSouphttpSrc * src); +static void gst_souphttp_src_finished_cb (SoupMessage * msg, + GstSouphttpSrc * src); + + static void _do_init (GType type) { @@ -140,6 +222,15 @@ gst_souphttp_src_class_init (GstSouphttpSrcClass * klass) g_param_spec_string ("user-agent", "User-Agent", "Value of the User-Agent HTTP request header field", DEFAULT_USER_AGENT, G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_AUTOMATIC_REDIRECT, + g_param_spec_boolean ("automatic-redirect", "automatic-redirect", + "Automatically follow HTTP redirects (HTTP Status Code 3xx)", + TRUE, G_PARAM_READWRITE)); + g_object_class_install_property (gobject_class, + PROP_PROXY, + g_param_spec_string ("proxy", "Proxy", + "HTTP proxy server URI", "", G_PARAM_READWRITE)); /* icecast stuff */ g_object_class_install_property (gobject_class, @@ -187,6 +278,8 @@ static void gst_souphttp_src_init (GstSouphttpSrc * src, GstSouphttpSrcClass * g_class) { src->location = NULL; + src->proxy = NULL; + src->automatic_redirect = TRUE; src->user_agent = g_strdup (DEFAULT_USER_AGENT); src->icy_caps = NULL; src->iradio_mode = FALSE; @@ -215,6 +308,10 @@ gst_souphttp_src_dispose (GObject * gobject) src->location = NULL; g_free (src->user_agent); src->user_agent = NULL; + if (src->proxy != NULL) { + soup_uri_free (src->proxy); + src->proxy = NULL; + } g_free (src->iradio_name); src->iradio_name = NULL; g_free (src->iradio_genre); @@ -265,6 +362,25 @@ gst_souphttp_src_set_property (GObject * object, guint prop_id, default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; + case PROP_AUTOMATIC_REDIRECT: + src->automatic_redirect = g_value_get_boolean (value); + break; + case PROP_PROXY: + { + const gchar *proxy; + + proxy = g_value_get_string (value); + + if (proxy == NULL) { + GST_WARNING ("proxy property cannot be NULL"); + goto done; + } + if (!gst_souphttp_src_set_proxy (src, proxy)) { + GST_WARNING ("badly formatted proxy URI"); + goto done; + } + break; + } } done: return; @@ -283,6 +399,19 @@ gst_souphttp_src_get_property (GObject * object, guint prop_id, case PROP_USER_AGENT: g_value_set_string (value, src->user_agent); break; + case PROP_AUTOMATIC_REDIRECT: + g_value_set_boolean (value, src->automatic_redirect); + break; + case PROP_PROXY: + if (src->proxy == NULL) + g_value_set_string (value, ""); + else { + char *proxy = soup_uri_to_string (src->proxy, FALSE); + + g_value_set_string (value, proxy); + free (proxy); + } + break; case PROP_IRADIO_MODE: g_value_set_boolean (value, src->iradio_mode); break; @@ -314,247 +443,64 @@ gst_souphttp_src_unicodify (const gchar * str) return gst_tag_freeform_string_to_utf8 (str, -1, env_vars); } -static GstFlowReturn -gst_souphttp_src_create (GstPushSrc * psrc, GstBuffer ** outbuf) -{ - GstSouphttpSrc *src; - - src = GST_SOUPHTTP_SRC (psrc); - - if (src->msg && (src->request_position != src->read_position)) { - if (src->msg->status == SOUP_MESSAGE_STATUS_IDLE) { - soup_add_range_header (src, src->request_position); - } else { - GST_DEBUG_OBJECT (src, "Seek from position %" G_GUINT64_FORMAT - " to %" G_GUINT64_FORMAT ": requeueing connection request", - src->read_position, src->request_position); - soup_session_cancel_message (src->session, src->msg); - src->msg = NULL; - } - } - if (!src->msg) { - src->msg = soup_message_new (SOUP_METHOD_GET, src->location); - if (!src->msg) { - GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, - (NULL), ("Error parsing URL \"%s\"", src->location)); - return GST_FLOW_ERROR; - } - soup_message_add_header (src->msg->request_headers, "Connection", "close"); - if (src->user_agent) { - soup_message_add_header (src->msg->request_headers, "User-Agent", - src->user_agent); - } - if (src->iradio_mode) { - soup_message_add_header (src->msg->request_headers, "icy-metadata", "1"); - } - - g_signal_connect (src->msg, "got_headers", - G_CALLBACK (soup_got_headers), src); - g_signal_connect (src->msg, "got_body", G_CALLBACK (soup_got_body), src); - g_signal_connect (src->msg, "finished", G_CALLBACK (soup_finished), src); - g_signal_connect (src->msg, "got_chunk", G_CALLBACK (soup_got_chunk), src); - soup_message_set_flags (src->msg, SOUP_MESSAGE_OVERWRITE_CHUNKS); - soup_add_range_header (src, src->request_position); - } - - src->ret = GST_FLOW_CUSTOM_ERROR; - src->outbuf = outbuf; - do { - if (src->interrupted) { - soup_session_cancel_message (src->session, src->msg); - src->msg = NULL; - break; - } - if (!src->msg) { - GST_DEBUG_OBJECT (src, "EOS reached"); - break; - } - - switch (src->msg->status) { - case SOUP_MESSAGE_STATUS_IDLE: - GST_DEBUG_OBJECT (src, "Queueing connection request"); - soup_session_queue_message (src->session, src->msg, soup_response, src); - break; - case SOUP_MESSAGE_STATUS_FINISHED: - GST_DEBUG_OBJECT (src, "Connection closed"); - soup_session_cancel_message (src->session, src->msg); - src->msg = NULL; - break; - case SOUP_MESSAGE_STATUS_QUEUED: - break; - case SOUP_MESSAGE_STATUS_CONNECTING: - case SOUP_MESSAGE_STATUS_RUNNING: - default: - soup_message_io_unpause (src->msg); - break; - } - - if (src->ret == GST_FLOW_CUSTOM_ERROR) - g_main_loop_run (src->loop); - } while (src->ret == GST_FLOW_CUSTOM_ERROR); - - if (src->ret == GST_FLOW_CUSTOM_ERROR) - src->ret = GST_FLOW_UNEXPECTED; - return src->ret; -} - -static gboolean -gst_souphttp_src_start (GstBaseSrc * bsrc) -{ - GstSouphttpSrc *src = GST_SOUPHTTP_SRC (bsrc); - - GST_DEBUG_OBJECT (src, "start(\"%s\")", src->location); - - if (!src->location) { - GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, - (NULL), ("Missing location property")); - return FALSE; - } - - src->context = g_main_context_new (); - - src->loop = g_main_loop_new (src->context, TRUE); - if (!src->loop) { - GST_ELEMENT_ERROR (src, LIBRARY, INIT, - (NULL), ("Failed to start GMainLoop")); - g_main_context_unref (src->context); - return FALSE; - } - - src->session = - soup_session_async_new_with_options (SOUP_SESSION_ASYNC_CONTEXT, - src->context, NULL); - if (!src->session) { - GST_ELEMENT_ERROR (src, LIBRARY, INIT, - (NULL), ("Failed to create async session")); - return FALSE; - } - - return TRUE; -} - -/* Close the socket and associated resources - * used both to recover from errors and go to NULL state. */ -static gboolean -gst_souphttp_src_stop (GstBaseSrc * bsrc) -{ - GstSouphttpSrc *src; - - src = GST_SOUPHTTP_SRC (bsrc); - GST_DEBUG_OBJECT (src, "stop()"); - soup_session_close (src); - if (src->loop) { - g_main_loop_unref (src->loop); - g_main_context_unref (src->context); - src->loop = NULL; - src->context = NULL; - } - - return TRUE; -} - -/* Interrupt a blocking request. */ -static gboolean -gst_souphttp_src_unlock (GstBaseSrc * bsrc) -{ - GstSouphttpSrc *src; - - src = GST_SOUPHTTP_SRC (bsrc); - GST_DEBUG_OBJECT (src, "unlock()"); - - src->interrupted = TRUE; - if (src->loop) - g_main_loop_quit (src->loop); - return TRUE; -} - -/* Interrupt interrupt. */ -static gboolean -gst_souphttp_src_unlock_stop (GstBaseSrc * bsrc) -{ - GstSouphttpSrc *src; - - src = GST_SOUPHTTP_SRC (bsrc); - GST_DEBUG_OBJECT (src, "unlock_stop()"); - - src->interrupted = FALSE; - return TRUE; -} - -static gboolean -gst_souphttp_src_get_size (GstBaseSrc * bsrc, guint64 * size) -{ - GstSouphttpSrc *src; - - src = GST_SOUPHTTP_SRC (bsrc); - - if (src->have_size) { - GST_DEBUG_OBJECT (src, "get_size() = %" G_GUINT64_FORMAT, - src->content_size); - *size = src->content_size; - return TRUE; - } - GST_DEBUG_OBJECT (src, "get_size() = FALSE"); - return FALSE; -} - -static gboolean -gst_souphttp_src_is_seekable (GstBaseSrc * bsrc) +static void +gst_souphttp_src_cancel_message (GstSouphttpSrc * src) { - GstSouphttpSrc *src = GST_SOUPHTTP_SRC (bsrc); - - return src->seekable; + soup_session_cancel_message (src->session, src->msg, SOUP_STATUS_CANCELLED); + src->session_io_status = GST_SOUPHTTP_SRC_SESSION_IO_STATUS_IDLE; + src->msg = NULL; } -static gboolean -gst_souphttp_src_do_seek (GstBaseSrc * bsrc, GstSegment * segment) +static void +gst_souphttp_src_queue_message (GstSouphttpSrc * src) { - GstSouphttpSrc *src = GST_SOUPHTTP_SRC (bsrc); - - GST_DEBUG_OBJECT (src, "do_seek(%" G_GUINT64_FORMAT ")", segment->start); - - if (src->read_position == segment->start) - return TRUE; - - if (!src->seekable) - return FALSE; - - /* Wait for create() to handle the jump in offset. */ - src->request_position = segment->start; - return TRUE; + soup_session_queue_message (src->session, src->msg, + (SoupSessionCallback) gst_souphttp_src_response_cb, src); + src->session_io_status = GST_SOUPHTTP_SRC_SESSION_IO_STATUS_QUEUED; } static gboolean -soup_add_range_header (GstSouphttpSrc * src, guint64 offset) +gst_souphttp_src_add_range_header (GstSouphttpSrc * src, guint64 offset) { gchar buf[64]; gint rc; - soup_message_remove_header (src->msg->request_headers, "Range"); + soup_message_headers_remove (src->msg->request_headers, "Range"); if (offset) { rc = g_snprintf (buf, sizeof (buf), "bytes=%" G_GUINT64_FORMAT "-", offset); if (rc > sizeof (buf) || rc < 0) return FALSE; - soup_message_add_header (src->msg->request_headers, "Range", buf); + soup_message_headers_append (src->msg->request_headers, "Range", buf); } src->read_position = offset; return TRUE; } -static gboolean -gst_souphttp_src_set_location (GstSouphttpSrc * src, const gchar * uri) +static void +gst_souphttp_src_session_unpause_message (GstSouphttpSrc * src) { - if (src->location) { - g_free (src->location); - src->location = NULL; - } - src->location = g_strdup (uri); + soup_session_unpause_message (src->session, src->msg); +} - return TRUE; +static void +gst_souphttp_src_session_pause_message (GstSouphttpSrc * src) +{ + soup_session_pause_message (src->session, src->msg); } static void -soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) +gst_souphttp_src_session_close (GstSouphttpSrc * src) +{ + if (src->session) { + soup_session_abort (src->session); /* This unrefs the message. */ + g_object_unref (src->session); + src->session = NULL; + src->msg = NULL; + } +} + +static void +gst_souphttp_src_got_headers_cb (SoupMessage * msg, GstSouphttpSrc * src) { const char *value; GstTagList *tag_list; @@ -563,14 +509,23 @@ soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) GST_DEBUG_OBJECT (src, "got headers"); + if (src->automatic_redirect && SOUP_STATUS_IS_REDIRECTION (msg->status_code)) { + GST_DEBUG_OBJECT (src, "%u redirect to \"%s\"", msg->status_code, + soup_message_headers_get (msg->response_headers, "Location")); + return; + } + + src->session_io_status = GST_SOUPHTTP_SRC_SESSION_IO_STATUS_RUNNING; + /* Parse Content-Length. */ - value = soup_message_get_header (msg->response_headers, "Content-Length"); - if (value != NULL) { - newsize = src->request_position + g_ascii_strtoull (value, NULL, 10); + if (soup_message_headers_get_encoding (msg->response_headers) == + SOUP_ENCODING_CONTENT_LENGTH) { + newsize = src->request_position + + soup_message_headers_get_content_length (msg->response_headers); if (!src->have_size || (src->content_size != newsize)) { src->content_size = newsize; src->have_size = TRUE; - GST_DEBUG_OBJECT (src, "size = %llu", src->content_size); + GST_DEBUG_OBJECT (src, "size = %" G_GUINT64_FORMAT, src->content_size); basesrc = GST_BASE_SRC_CAST (src); gst_segment_set_duration (&basesrc->segment, GST_FORMAT_BYTES, @@ -585,7 +540,7 @@ soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) tag_list = gst_tag_list_new (); if ((value = - soup_message_get_header (msg->response_headers, + soup_message_headers_get (msg->response_headers, "icy-metaint")) != NULL) { gint icy_metaint = atoi (value); @@ -596,7 +551,7 @@ soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) } if ((value = - soup_message_get_header (msg->response_headers, + soup_message_headers_get (msg->response_headers, "icy-name")) != NULL) { g_free (src->iradio_name); src->iradio_name = gst_souphttp_src_unicodify (value); @@ -607,7 +562,7 @@ soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) } } if ((value = - soup_message_get_header (msg->response_headers, + soup_message_headers_get (msg->response_headers, "icy-genre")) != NULL) { g_free (src->iradio_genre); src->iradio_genre = gst_souphttp_src_unicodify (value); @@ -617,8 +572,8 @@ soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) src->iradio_genre, NULL); } } - if ((value = - soup_message_get_header (msg->response_headers, "icy-url")) != NULL) { + if ((value = soup_message_headers_get (msg->response_headers, "icy-url")) + != NULL) { g_free (src->iradio_url); src->iradio_url = gst_souphttp_src_unicodify (value); if (src->iradio_url) { @@ -636,7 +591,7 @@ soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) } /* Handle HTTP errors. */ - soup_parse_status (msg, src); + gst_souphttp_src_parse_status (msg, src); /* Check if Range header was respected. */ if (src->ret == GST_FLOW_CUSTOM_ERROR && @@ -651,27 +606,37 @@ soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) /* Have body. Signal EOS. */ static void -soup_got_body (SoupMessage * msg, GstSouphttpSrc * src) +gst_souphttp_src_got_body_cb (SoupMessage * msg, GstSouphttpSrc * src) { - if (msg != src->msg) { + if (G_UNLIKELY (msg != src->msg)) { GST_DEBUG_OBJECT (src, "got body, but not for current message"); return; } + if (G_UNLIKELY (src->session_io_status != + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_RUNNING)) { + /* Probably a redirect. */ + return; + } GST_DEBUG_OBJECT (src, "got body"); src->ret = GST_FLOW_UNEXPECTED; if (src->loop) g_main_loop_quit (src->loop); - soup_message_io_pause (msg); + gst_souphttp_src_session_pause_message (src); } /* Finished. Signal EOS. */ static void -soup_finished (SoupMessage * msg, GstSouphttpSrc * src) +gst_souphttp_src_finished_cb (SoupMessage * msg, GstSouphttpSrc * src) { - if (msg != src->msg) { + if (G_UNLIKELY (msg != src->msg)) { GST_DEBUG_OBJECT (src, "finished, but not for current message"); return; } + if (G_UNLIKELY (src->session_io_status != + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_RUNNING)) { + /* Probably a redirect. */ + return; + } GST_DEBUG_OBJECT (src, "finished"); src->ret = GST_FLOW_UNEXPECTED; if (src->loop) @@ -679,53 +644,66 @@ soup_finished (SoupMessage * msg, GstSouphttpSrc * src) } static void -soup_got_chunk (SoupMessage * msg, GstSouphttpSrc * src) +gst_souphttp_src_got_chunk_cb (SoupMessage * msg, SoupBuffer * chunk, + GstSouphttpSrc * src) { GstBaseSrc *basesrc; guint64 new_position; + const char *data; + gsize length; if (G_UNLIKELY (msg != src->msg)) { GST_DEBUG_OBJECT (src, "got chunk, but not for current message"); return; } + if (G_UNLIKELY (src->session_io_status != + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_RUNNING)) { + /* Probably a redirect. */ + return; + } basesrc = GST_BASE_SRC_CAST (src); - GST_DEBUG_OBJECT (src, "got chunk of %d bytes", msg->response.length); + data = chunk->data; + length = chunk->length; + GST_DEBUG_OBJECT (src, "got chunk of %d bytes", length); /* Create the buffer. */ src->ret = gst_pad_alloc_buffer (GST_BASE_SRC_PAD (basesrc), - basesrc->segment.last_stop, msg->response.length, + basesrc->segment.last_stop, length, GST_PAD_CAPS (GST_BASE_SRC_PAD (basesrc)), src->outbuf); if (G_LIKELY (src->ret == GST_FLOW_OK)) { - memcpy (GST_BUFFER_DATA (*src->outbuf), msg->response.body, - msg->response.length); - new_position = src->read_position + msg->response.length; + memcpy (GST_BUFFER_DATA (*src->outbuf), data, length); + new_position = src->read_position + length; if (G_LIKELY (src->request_position == src->read_position)) src->request_position = new_position; src->read_position = new_position; } g_main_loop_quit (src->loop); - soup_message_io_pause (msg); + gst_souphttp_src_session_pause_message (src); } static void -soup_response (SoupMessage * msg, gpointer user_data) +gst_souphttp_src_response_cb (SoupSession * session, SoupMessage * msg, + GstSouphttpSrc * src) { - GstSouphttpSrc *src = (GstSouphttpSrc *) user_data; - - if (msg != src->msg) { + if (G_UNLIKELY (msg != src->msg)) { GST_DEBUG_OBJECT (src, "got response %d: %s, but not for current message", msg->status_code, msg->reason_phrase); return; } + if (G_UNLIKELY (src->session_io_status != + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_RUNNING)) { + /* Probably a redirect. */ + return; + } GST_DEBUG_OBJECT (src, "got response %d: %s", msg->status_code, msg->reason_phrase); - soup_parse_status (msg, src); + gst_souphttp_src_parse_status (msg, src); g_main_loop_quit (src->loop); } static void -soup_parse_status (SoupMessage * msg, GstSouphttpSrc * src) +gst_souphttp_src_parse_status (SoupMessage * msg, GstSouphttpSrc * src) { if (SOUP_STATUS_IS_TRANSPORT_ERROR (msg->status_code)) { switch (msg->status_code) { @@ -761,6 +739,7 @@ soup_parse_status (SoupMessage * msg, GstSouphttpSrc * src) break; } } else if (SOUP_STATUS_IS_CLIENT_ERROR (msg->status_code) || + SOUP_STATUS_IS_REDIRECTION (msg->status_code) || SOUP_STATUS_IS_SERVER_ERROR (msg->status_code)) { /* Report HTTP error. */ GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, @@ -770,15 +749,245 @@ soup_parse_status (SoupMessage * msg, GstSouphttpSrc * src) } } -static void -soup_session_close (GstSouphttpSrc * src) +static GstFlowReturn +gst_souphttp_src_create (GstPushSrc * psrc, GstBuffer ** outbuf) { - if (src->session) { - soup_session_abort (src->session); /* This unrefs the message. */ - g_object_unref (src->session); - src->session = NULL; - src->msg = NULL; + GstSouphttpSrc *src; + + src = GST_SOUPHTTP_SRC (psrc); + + if (src->msg && (src->request_position != src->read_position)) { + if (src->session_io_status == GST_SOUPHTTP_SRC_SESSION_IO_STATUS_IDLE) { + gst_souphttp_src_add_range_header (src, src->request_position); + } else { + GST_DEBUG_OBJECT (src, "Seek from position %" G_GUINT64_FORMAT + " to %" G_GUINT64_FORMAT ": requeueing connection request", + src->read_position, src->request_position); + gst_souphttp_src_cancel_message (src); + } + } + if (!src->msg) { + src->msg = soup_message_new (SOUP_METHOD_GET, src->location); + if (!src->msg) { + GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, + (NULL), ("Error parsing URL \"%s\"", src->location)); + return GST_FLOW_ERROR; + } + src->session_io_status = GST_SOUPHTTP_SRC_SESSION_IO_STATUS_IDLE; + soup_message_headers_append (src->msg->request_headers, "Connection", + "close"); + if (src->user_agent) { + soup_message_headers_append (src->msg->request_headers, "User-Agent", + src->user_agent); + } + if (src->iradio_mode) { + soup_message_headers_append (src->msg->request_headers, "icy-metadata", + "1"); + } + + g_signal_connect (src->msg, "got_headers", + G_CALLBACK (gst_souphttp_src_got_headers_cb), src); + g_signal_connect (src->msg, "got_body", + G_CALLBACK (gst_souphttp_src_got_body_cb), src); + g_signal_connect (src->msg, "finished", + G_CALLBACK (gst_souphttp_src_finished_cb), src); + g_signal_connect (src->msg, "got_chunk", + G_CALLBACK (gst_souphttp_src_got_chunk_cb), src); + soup_message_set_flags (src->msg, SOUP_MESSAGE_OVERWRITE_CHUNKS | + src->automatic_redirect ? 0 : SOUP_MESSAGE_NO_REDIRECT); + gst_souphttp_src_add_range_header (src, src->request_position); + } + + src->ret = GST_FLOW_CUSTOM_ERROR; + src->outbuf = outbuf; + do { + if (src->interrupted) { + gst_souphttp_src_cancel_message (src); + break; + } + if (!src->msg) { + GST_DEBUG_OBJECT (src, "EOS reached"); + break; + } + + switch (src->session_io_status) { + case GST_SOUPHTTP_SRC_SESSION_IO_STATUS_IDLE: + GST_DEBUG_OBJECT (src, "Queueing connection request"); + gst_souphttp_src_queue_message (src); + break; + case GST_SOUPHTTP_SRC_SESSION_IO_STATUS_FINISHED: + GST_DEBUG_OBJECT (src, "Connection closed"); + gst_souphttp_src_cancel_message (src); + break; + case GST_SOUPHTTP_SRC_SESSION_IO_STATUS_QUEUED: + break; + case GST_SOUPHTTP_SRC_SESSION_IO_STATUS_RUNNING: + gst_souphttp_src_session_unpause_message (src); + break; + } + + if (src->ret == GST_FLOW_CUSTOM_ERROR) + g_main_loop_run (src->loop); + } while (src->ret == GST_FLOW_CUSTOM_ERROR); + + if (src->ret == GST_FLOW_CUSTOM_ERROR) + src->ret = GST_FLOW_UNEXPECTED; + return src->ret; +} + +static gboolean +gst_souphttp_src_start (GstBaseSrc * bsrc) +{ + GstSouphttpSrc *src = GST_SOUPHTTP_SRC (bsrc); + + GST_DEBUG_OBJECT (src, "start(\"%s\")", src->location); + + if (!src->location) { + GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, + (NULL), ("Missing location property")); + return FALSE; + } + + src->context = g_main_context_new (); + + src->loop = g_main_loop_new (src->context, TRUE); + if (!src->loop) { + GST_ELEMENT_ERROR (src, LIBRARY, INIT, + (NULL), ("Failed to start GMainLoop")); + g_main_context_unref (src->context); + return FALSE; + } + + if (src->proxy == NULL) + src->session = + soup_session_async_new_with_options (SOUP_SESSION_ASYNC_CONTEXT, + src->context, NULL); + else + src->session = + soup_session_async_new_with_options (SOUP_SESSION_ASYNC_CONTEXT, + src->context, SOUP_SESSION_PROXY_URI, src->proxy, NULL); + if (!src->session) { + GST_ELEMENT_ERROR (src, LIBRARY, INIT, + (NULL), ("Failed to create async session")); + return FALSE; + } + + return TRUE; +} + +/* Close the socket and associated resources + * used both to recover from errors and go to NULL state. */ +static gboolean +gst_souphttp_src_stop (GstBaseSrc * bsrc) +{ + GstSouphttpSrc *src; + + src = GST_SOUPHTTP_SRC (bsrc); + GST_DEBUG_OBJECT (src, "stop()"); + gst_souphttp_src_session_close (src); + if (src->loop) { + g_main_loop_unref (src->loop); + g_main_context_unref (src->context); + src->loop = NULL; + src->context = NULL; + } + + return TRUE; +} + +/* Interrupt a blocking request. */ +static gboolean +gst_souphttp_src_unlock (GstBaseSrc * bsrc) +{ + GstSouphttpSrc *src; + + src = GST_SOUPHTTP_SRC (bsrc); + GST_DEBUG_OBJECT (src, "unlock()"); + + src->interrupted = TRUE; + if (src->loop) + g_main_loop_quit (src->loop); + return TRUE; +} + +/* Interrupt interrupt. */ +static gboolean +gst_souphttp_src_unlock_stop (GstBaseSrc * bsrc) +{ + GstSouphttpSrc *src; + + src = GST_SOUPHTTP_SRC (bsrc); + GST_DEBUG_OBJECT (src, "unlock_stop()"); + + src->interrupted = FALSE; + return TRUE; +} + +static gboolean +gst_souphttp_src_get_size (GstBaseSrc * bsrc, guint64 * size) +{ + GstSouphttpSrc *src; + + src = GST_SOUPHTTP_SRC (bsrc); + + if (src->have_size) { + GST_DEBUG_OBJECT (src, "get_size() = %" G_GUINT64_FORMAT, + src->content_size); + *size = src->content_size; + return TRUE; } + GST_DEBUG_OBJECT (src, "get_size() = FALSE"); + return FALSE; +} + +static gboolean +gst_souphttp_src_is_seekable (GstBaseSrc * bsrc) +{ + GstSouphttpSrc *src = GST_SOUPHTTP_SRC (bsrc); + + return src->seekable; +} + +static gboolean +gst_souphttp_src_do_seek (GstBaseSrc * bsrc, GstSegment * segment) +{ + GstSouphttpSrc *src = GST_SOUPHTTP_SRC (bsrc); + + GST_DEBUG_OBJECT (src, "do_seek(%" G_GUINT64_FORMAT ")", segment->start); + + if (src->read_position == segment->start) + return TRUE; + + if (!src->seekable) + return FALSE; + + /* Wait for create() to handle the jump in offset. */ + src->request_position = segment->start; + return TRUE; +} + +static gboolean +gst_souphttp_src_set_location (GstSouphttpSrc * src, const gchar * uri) +{ + if (src->location) { + g_free (src->location); + src->location = NULL; + } + src->location = g_strdup (uri); + + return TRUE; +} + +static gboolean +gst_souphttp_src_set_proxy (GstSouphttpSrc * src, const gchar * uri) +{ + if (src->proxy) { + soup_uri_free (src->proxy); + src->proxy = NULL; + } + src->proxy = soup_uri_new (uri); + + return TRUE; } static guint @@ -824,9 +1033,7 @@ gst_souphttp_src_uri_handler_init (gpointer g_iface, gpointer iface_data) static gboolean plugin_init (GstPlugin * plugin) { - /* note: do not upgrade rank before we depend on a libsoup version where - * icecast is supported properly out of the box */ - return gst_element_register (plugin, "souphttpsrc", GST_RANK_NONE, + return gst_element_register (plugin, "souphttpsrc", GST_RANK_MARGINAL, GST_TYPE_SOUPHTTP_SRC); } diff --git a/ext/soup/gstsouphttpsrc.h b/ext/soup/gstsouphttpsrc.h index 21aff958..9c24382a 100644 --- a/ext/soup/gstsouphttpsrc.h +++ b/ext/soup/gstsouphttpsrc.h @@ -37,14 +37,25 @@ G_BEGIN_DECLS typedef struct _GstSouphttpSrc GstSouphttpSrc; typedef struct _GstSouphttpSrcClass GstSouphttpSrcClass; +typedef enum { + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_IDLE, + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_QUEUED, + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_RUNNING, + GST_SOUPHTTP_SRC_SESSION_IO_STATUS_FINISHED, +} GstSouphttpSrcSessionIOStatus; + struct _GstSouphttpSrc { GstPushSrc element; gchar * location; /* Full URI. */ gchar * user_agent; /* User-Agent HTTP header. */ + gboolean automatic_redirect; /* Follow redirects. */ + SoupURI * proxy; /* HTTP proxy URI. */ GMainContext * context; /* I/O context. */ GMainLoop * loop; /* Event loop. */ SoupSession * session; /* Async context. */ + GstSouphttpSrcSessionIOStatus session_io_status; + /* Async I/O status. */ SoupMessage * msg; /* Request message. */ GstFlowReturn ret; /* Return code from callback. */ GstBuffer ** outbuf; /* Return buffer allocated by callback. */ diff --git a/tests/check/Makefile.am b/tests/check/Makefile.am index 387098c1..ebdbd097 100644 --- a/tests/check/Makefile.am +++ b/tests/check/Makefile.am @@ -15,6 +15,8 @@ TESTS_ENVIRONMENT = \ GST_PLUGIN_PATH=$(top_builddir)/gst:$(top_builddir)/sys:$(top_builddir)/ext:$(GSTPB_PLUGINS_DIR):$(GST_PLUGINS_DIR) \ STATE_IGNORE_ELEMENTS="vcdsrc nassink glimagesink" +EXTRA_DIST = test-cert.pem test-key.pem + plugindir = $(libdir)/gstreamer-@GST_MAJORMINOR@ # override to _not_ install the test plugins @@ -45,6 +47,12 @@ else check_neon = endif +if USE_SOUP +check_soup = elements/souphttpsrc +else +check_soup = +endif + if USE_TIMIDITY check_timidity=elements/timidity else @@ -64,6 +72,7 @@ check_PROGRAMS = \ $(check_gio) \ $(check_mpeg2enc) \ $(check_neon) \ + $(check_soup) \ $(check_timidity) \ elements/bpwsinc \ elements/equalizer \ @@ -90,3 +99,5 @@ elements_timidity_LDADD = $(GST_BASE_LIBS) $(LDADD) pipelines_gio_CFLAGS = $(GIO_CFLAGS) $(AM_CFLAGS) pipelines_gio_LDADD = $(GIO_LIBS) $(LDADD) +elements_souphttpsrc_CFLAGS = $(SOUP_CFLAGS) $(AM_CFLAGS) +elements_souphttpsrc_LDADD = $(SOUP_LIBS) $(LDADD) diff --git a/tests/check/elements/.gitignore b/tests/check/elements/.gitignore index bdeb9a37..ac1d683f 100644 --- a/tests/check/elements/.gitignore +++ b/tests/check/elements/.gitignore @@ -8,6 +8,7 @@ interleave lpwsinc mpeg2enc neonhttpsrc +souphttpsrc rganalysis rglimiter rgvolume diff --git a/tests/check/elements/souphttpsrc.c b/tests/check/elements/souphttpsrc.c new file mode 100644 index 00000000..8f85c1df --- /dev/null +++ b/tests/check/elements/souphttpsrc.c @@ -0,0 +1,408 @@ +/* GStreamer unit tests for the souphttpsrc element + * Copyright (C) 2006-2007 Tim-Philipp Müller <tim centricular net> + * Copyright (C) 2008 Wouter Cloetens <wouter@mind.be> + * Copyright (C) 2001-2003, Ximian, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <glib.h> +#include <libsoup/soup-address.h> +#include <libsoup/soup-message.h> +#include <libsoup/soup-server.h> +#include <gst/check/gstcheck.h> + +static int http_port = 0, https_port = 0; +gboolean redirect = TRUE; + +static int run_server (int *http_port, int *https_port); + + +static void +handoff_cb (GstElement * fakesink, GstBuffer * buf, GstPad * pad, + GstBuffer ** p_outbuf) +{ + GST_LOG ("handoff, buf = %p", buf); + if (*p_outbuf == NULL) + *p_outbuf = gst_buffer_ref (buf); +} + +int +run_test (const char *format, ...) +{ + GstStateChangeReturn ret; + GstElement *pipe, *src, *sink; + GstBuffer *buf = NULL; + GstMessage *msg; + gchar *url; + va_list args; + int rc = -1; + + pipe = gst_pipeline_new (NULL); + + src = gst_element_factory_make ("souphttpsrc", NULL); + fail_unless (src != NULL); + + sink = gst_element_factory_make ("fakesink", NULL); + fail_unless (sink != NULL); + + gst_bin_add (GST_BIN (pipe), src); + gst_bin_add (GST_BIN (pipe), sink); + fail_unless (gst_element_link (src, sink)); + + if (http_port == 0) { + GST_DEBUG ("failed to start soup http server"); + } + fail_unless (http_port != 0); + va_start (args, format); + g_vasprintf (&url, format, args); + va_end (args); + fail_unless (url != NULL); + g_object_set (src, "location", url, NULL); + g_free (url); + + g_object_set (src, "automatic-redirect", redirect, NULL); + g_object_set (sink, "signal-handoffs", TRUE, NULL); + g_signal_connect (sink, "preroll-handoff", G_CALLBACK (handoff_cb), &buf); + + ret = gst_element_set_state (pipe, GST_STATE_PAUSED); + if (ret != GST_STATE_CHANGE_ASYNC) { + GST_DEBUG ("failed to start up soup http src, ret = %d", ret); + goto done; + } + + gst_element_set_state (pipe, GST_STATE_PLAYING); + msg = gst_bus_poll (GST_ELEMENT_BUS (pipe), + GST_MESSAGE_EOS | GST_MESSAGE_ERROR, -1); + if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) { + gchar *debug = NULL; + GError *err = NULL; + + gst_message_parse_error (msg, &err, &debug); + GST_INFO ("error: %s", err->message); + if (g_str_has_suffix (err->message, "Not Found")) + rc = 404; + else if (g_str_has_suffix (err->message, "Forbidden")) + rc = 403; + else if (g_str_has_suffix (err->message, "Found")) + rc = 302; + GST_INFO ("debug: %s", debug); + g_error_free (err); + g_free (debug); + gst_message_unref (msg); + goto done; + } + gst_message_unref (msg); + + /* don't wait for more than 10 seconds */ + ret = gst_element_get_state (pipe, NULL, NULL, 10 * GST_SECOND); + GST_LOG ("ret = %u", ret); + + if (buf == NULL) { + /* we want to test the buffer offset, nothing else; if there's a failure + * it might be for lots of reasons (no network connection, whatever), we're + * not interested in those */ + GST_DEBUG ("didn't manage to get data within 10 seconds, skipping test"); + goto done; + } + + GST_DEBUG ("buffer offset = %" G_GUINT64_FORMAT, GST_BUFFER_OFFSET (buf)); + + /* first buffer should have a 0 offset */ + fail_unless (GST_BUFFER_OFFSET (buf) == 0); + gst_buffer_unref (buf); + rc = 0; + +done: + + gst_element_set_state (pipe, GST_STATE_NULL); + gst_object_unref (pipe); + return rc; +} + +GST_START_TEST (test_first_buffer_has_offset) +{ + fail_unless (run_test ("http://127.0.0.1:%d/", http_port) == 0); +} + +GST_END_TEST; + +GST_START_TEST (test_not_found) +{ + fail_unless (run_test ("http://127.0.0.1:%d/404", http_port) == 404); +} + +GST_END_TEST; + +GST_START_TEST (test_forbidden) +{ + fail_unless (run_test ("http://127.0.0.1:%d/403", http_port) == 403); +} + +GST_END_TEST; + +GST_START_TEST (test_redirect_no) +{ + redirect = FALSE; + fail_unless (run_test ("http://127.0.0.1:%d/302", http_port) == 302); +} + +GST_END_TEST; + +GST_START_TEST (test_redirect_yes) +{ + redirect = TRUE; + fail_unless (run_test ("http://127.0.0.1:%d/302", http_port) == 0); +} + +GST_END_TEST; + +GST_START_TEST (test_https) +{ + if (!https_port) + GST_INFO ("Failed to start an HTTPS server; let's just skip this test."); + else + fail_unless (run_test ("https://127.0.0.1:%d/", https_port) == 0); +} + +GST_END_TEST; + +GST_START_TEST (test_icy_stream) +{ + GstElement *pipe, *src, *sink; + GstMessage *msg; + + pipe = gst_pipeline_new (NULL); + + src = gst_element_factory_make ("souphttpsrc", NULL); + fail_unless (src != NULL); + + sink = gst_element_factory_make ("fakesink", NULL); + fail_unless (sink != NULL); + + gst_bin_add (GST_BIN (pipe), src); + gst_bin_add (GST_BIN (pipe), sink); + fail_unless (gst_element_link (src, sink)); + + /* First try Virgin Radio Ogg stream, to see if there's connectivity and all + * (which is an attempt to work around the completely horrid error reporting + * and that we can't distinguish different types of failures here). */ + + g_object_set (src, "location", "http://ogg2.smgradio.com/vr32.ogg", NULL); + g_object_set (src, "num-buffers", 1, NULL); + gst_element_set_state (pipe, GST_STATE_PLAYING); + + msg = gst_bus_poll (GST_ELEMENT_BUS (pipe), + GST_MESSAGE_EOS | GST_MESSAGE_ERROR, -1); + if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) { + GST_INFO ("looks like there's no net connectivity or sgmradio.com is " + "down. In any case, let's just skip this test"); + gst_message_unref (msg); + goto done; + } + gst_message_unref (msg); + msg = NULL; + gst_element_set_state (pipe, GST_STATE_NULL); + + /* Now, if the ogg stream works, the mp3 shoutcast stream should work as + * well (time will tell if that's true) */ + + /* Virgin Radio 32kbps mp3 shoutcast stream */ + g_object_set (src, "location", "http://mp3-vr-32.smgradio.com:80/", NULL); + + + /* EOS after the first buffer */ + g_object_set (src, "num-buffers", 1, NULL); + + gst_element_set_state (pipe, GST_STATE_PLAYING); + msg = gst_bus_poll (GST_ELEMENT_BUS (pipe), + GST_MESSAGE_EOS | GST_MESSAGE_ERROR, -1); + + if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS) { + GST_DEBUG ("success, we're done here"); + gst_message_unref (msg); + goto done; + } + + { + GError *err = NULL; + + gst_message_parse_error (msg, &err, NULL); + gst_message_unref (msg); + g_error ("Error with ICY mp3 shoutcast stream: %s", err->message); + g_error_free (err); + } + +done: + + gst_element_set_state (pipe, GST_STATE_NULL); + gst_object_unref (pipe); +} + +GST_END_TEST; + +static Suite * +souphttpsrc_suite (void) +{ + g_type_init (); + g_thread_init (NULL); + + Suite *s = suite_create ("souphttpsrc"); + TCase *tc_chain = tcase_create ("general"); + + suite_add_tcase (s, tc_chain); + run_server (&http_port, &https_port); + tcase_add_test (tc_chain, test_first_buffer_has_offset); + tcase_add_test (tc_chain, test_https); + tcase_add_test (tc_chain, test_redirect_yes); + tcase_add_test (tc_chain, test_redirect_no); + tcase_add_test (tc_chain, test_not_found); + tcase_add_test (tc_chain, test_forbidden); + tcase_add_test (tc_chain, test_icy_stream); + + return s; +} + +GST_CHECK_MAIN (souphttpsrc); + +static void +do_get (SoupMessage * msg, const char *path) +{ + char *uri; + int buflen = 4096; + SoupKnownStatusCode status = SOUP_STATUS_OK; + + uri = soup_uri_to_string (soup_message_get_uri (msg), FALSE); + GST_DEBUG ("request: \"%s\"", uri); + + if (!strcmp (path, "/301")) + status = SOUP_STATUS_MOVED_PERMANENTLY; + else if (!strcmp (path, "/302")) + status = SOUP_STATUS_MOVED_TEMPORARILY; + else if (!strcmp (path, "/307")) + status = SOUP_STATUS_TEMPORARY_REDIRECT; + else if (!strcmp (path, "/403")) + status = SOUP_STATUS_FORBIDDEN; + else if (!strcmp (path, "/404")) + status = SOUP_STATUS_NOT_FOUND; + + if (SOUP_STATUS_IS_REDIRECTION (status)) { + char *redir_uri; + + redir_uri = g_strdup_printf ("%s-redirected", uri); + soup_message_headers_append (msg->response_headers, "Location", redir_uri); + g_free (redir_uri); + } + if (status != SOUP_STATUS_OK) + goto leave; + + if (msg->method == SOUP_METHOD_GET) { + char *buf; + + buf = g_malloc (buflen); + memset (buf, 0, buflen); + soup_message_body_append (msg->response_body, SOUP_MEMORY_TAKE, + buf, buflen); + } else { /* msg->method == SOUP_METHOD_HEAD */ + + char *length; + + /* We could just use the same code for both GET and + * HEAD. But we'll optimize and avoid the extra + * malloc. + */ + length = g_strdup_printf ("%lu", (gulong) buflen); + soup_message_headers_append (msg->response_headers, + "Content-Length", length); + g_free (length); + } + +leave: + soup_message_set_status (msg, status); + g_free (uri); +} + +static void +print_header (const char *name, const char *value, gpointer data) +{ + GST_DEBUG ("header: %s: %s", name, value); +} + +static void +server_callback (SoupServer * server, SoupMessage * msg, + const char *path, GHashTable * query, + SoupClientContext * context, gpointer data) +{ + GST_DEBUG ("%s %s HTTP/1.%d", msg->method, path, + soup_message_get_http_version (msg)); + soup_message_headers_foreach (msg->request_headers, print_header, NULL); + if (msg->request_body->length) + GST_DEBUG ("%s", msg->request_body->data); + + if (msg->method == SOUP_METHOD_GET || msg->method == SOUP_METHOD_HEAD) + do_get (msg, path); + else + soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED); + + GST_DEBUG (" -> %d %s", msg->status_code, msg->reason_phrase); +} + +int +run_server (int *http_port, int *https_port) +{ + SoupServer *server, *ssl_server; + int port = SOUP_ADDRESS_ANY_PORT; + int ssl_port = SOUP_ADDRESS_ANY_PORT; + const char *ssl_cert_file = "test-cert.pem", *ssl_key_file = "test-key.pem"; + static int server_running = 0; + + if (server_running) + return 0; + server_running = 1; + + *http_port = *https_port = 0; + + server = soup_server_new (SOUP_SERVER_PORT, port, NULL); + if (!server) { + GST_DEBUG ("Unable to bind to server port %d", port); + return 1; + } + *http_port = soup_server_get_port (server); + GST_INFO ("HTTP server listening on port %d", *http_port); + soup_server_add_handler (server, NULL, server_callback, NULL, NULL); + soup_server_run_async (server); + + if (ssl_cert_file && ssl_key_file) { + ssl_server = soup_server_new (SOUP_SERVER_PORT, ssl_port, + SOUP_SERVER_SSL_CERT_FILE, ssl_cert_file, + SOUP_SERVER_SSL_KEY_FILE, ssl_key_file, NULL); + + if (!ssl_server) { + GST_DEBUG ("Unable to bind to SSL server port %d", ssl_port); + return 1; + } + *https_port = soup_server_get_port (ssl_server); + GST_INFO ("HTTPS server listening on port %d", *https_port); + soup_server_add_handler (ssl_server, NULL, server_callback, NULL, NULL); + soup_server_run_async (ssl_server); + } + + return 0; +} |