/* GStreamer * Copyright (C) <2007> Wouter Cloetens * * 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 */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include "gstsouphttpsrc.h" #include GST_DEBUG_CATEGORY_STATIC (souphttpsrc_debug); #define GST_CAT_DEFAULT souphttpsrc_debug static const GstElementDetails gst_souphttp_src_details = GST_ELEMENT_DETAILS ("HTTP client source", "Source/Network", "Receive data as a client over the network via HTTP using SOUP", "Wouter Cloetens "); static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS_ANY); enum { PROP_0, PROP_LOCATION, PROP_USER_AGENT, PROP_IRADIO_MODE, PROP_IRADIO_NAME, PROP_IRADIO_GENRE, PROP_IRADIO_URL, PROP_IRADIO_TITLE }; #define DEFAULT_USER_AGENT "GStreamer souphttpsrc" static void gst_souphttp_src_uri_handler_init (gpointer g_iface, gpointer iface_data); static void gst_souphttp_src_init (GstSouphttpSrc * src, GstSouphttpSrcClass * g_class); static void gst_souphttp_src_dispose (GObject * gobject); static void gst_souphttp_src_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); static void gst_souphttp_src_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); static GstFlowReturn gst_souphttp_src_create (GstPushSrc * psrc, GstBuffer ** outbuf); static gboolean gst_souphttp_src_start (GstBaseSrc * bsrc); static gboolean gst_souphttp_src_stop (GstBaseSrc * bsrc); static gboolean gst_souphttp_src_get_size (GstBaseSrc * bsrc, guint64 * size); static gboolean gst_souphttp_src_is_seekable (GstBaseSrc * bsrc); static gboolean gst_souphttp_src_unlock (GstBaseSrc * bsrc); static gboolean gst_souphttp_src_unlock_stop (GstBaseSrc * bsrc); static gboolean gst_souphttp_src_set_location (GstSouphttpSrc * src, const gchar * uri); 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 char *gst_souphttp_src_unicodify (const char *str); static void _do_init (GType type) { static const GInterfaceInfo urihandler_info = { gst_souphttp_src_uri_handler_init, NULL, NULL }; g_type_add_interface_static (type, GST_TYPE_URI_HANDLER, &urihandler_info); GST_DEBUG_CATEGORY_INIT (souphttpsrc_debug, "souphttpsrc", 0, "SOUP HTTP src"); } GST_BOILERPLATE_FULL (GstSouphttpSrc, gst_souphttp_src, GstPushSrc, GST_TYPE_PUSH_SRC, _do_init); static void gst_souphttp_src_base_init (gpointer g_class) { GstElementClass *element_class = GST_ELEMENT_CLASS (g_class); gst_element_class_add_pad_template (element_class, gst_static_pad_template_get (&srctemplate)); gst_element_class_set_details (element_class, &gst_souphttp_src_details); } static void gst_souphttp_src_class_init (GstSouphttpSrcClass * klass) { GObjectClass *gobject_class; GstBaseSrcClass *gstbasesrc_class; GstPushSrcClass *gstpushsrc_class; gobject_class = (GObjectClass *) klass; gstbasesrc_class = (GstBaseSrcClass *) klass; gstpushsrc_class = (GstPushSrcClass *) klass; gobject_class->set_property = gst_souphttp_src_set_property; gobject_class->get_property = gst_souphttp_src_get_property; gobject_class->dispose = gst_souphttp_src_dispose; g_object_class_install_property (gobject_class, PROP_LOCATION, g_param_spec_string ("location", "Location", "Location to read from", "", G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_USER_AGENT, g_param_spec_string ("user-agent", "User-Agent", "Value of the User-Agent HTTP request header field", DEFAULT_USER_AGENT, G_PARAM_READWRITE)); /* icecast stuff */ g_object_class_install_property (gobject_class, PROP_IRADIO_MODE, g_param_spec_boolean ("iradio-mode", "iradio-mode", "Enable internet radio mode (extraction of shoutcast/icecast metadata)", FALSE, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, PROP_IRADIO_NAME, g_param_spec_string ("iradio-name", "iradio-name", "Name of the stream", NULL, G_PARAM_READABLE)); g_object_class_install_property (gobject_class, PROP_IRADIO_GENRE, g_param_spec_string ("iradio-genre", "iradio-genre", "Genre of the stream", NULL, G_PARAM_READABLE)); g_object_class_install_property (gobject_class, PROP_IRADIO_URL, g_param_spec_string ("iradio-url", "iradio-url", "Homepage URL for radio stream", NULL, G_PARAM_READABLE)); g_object_class_install_property (gobject_class, PROP_IRADIO_TITLE, g_param_spec_string ("iradio-title", "iradio-title", "Name of currently playing song", NULL, G_PARAM_READABLE)); gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_souphttp_src_start); gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_souphttp_src_stop); gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR (gst_souphttp_src_unlock); gstbasesrc_class->unlock_stop = GST_DEBUG_FUNCPTR (gst_souphttp_src_unlock_stop); gstbasesrc_class->get_size = GST_DEBUG_FUNCPTR (gst_souphttp_src_get_size); gstbasesrc_class->is_seekable = GST_DEBUG_FUNCPTR (gst_souphttp_src_is_seekable); gstpushsrc_class->create = GST_DEBUG_FUNCPTR (gst_souphttp_src_create); GST_DEBUG_CATEGORY_INIT (souphttpsrc_debug, "souphttpsrc", 0, "SOUP HTTP Client Source"); } static void gst_souphttp_src_init (GstSouphttpSrc * src, GstSouphttpSrcClass * g_class) { src->location = NULL; src->user_agent = g_strdup (DEFAULT_USER_AGENT); src->icy_caps = NULL; src->iradio_mode = FALSE; src->iradio_name = NULL; src->iradio_genre = NULL; src->iradio_url = NULL; src->iradio_title = NULL; src->loop = NULL; src->context = NULL; src->session = NULL; src->msg = NULL; src->interrupted = FALSE; src->have_size = FALSE; src->read_position = 0; } static void gst_souphttp_src_dispose (GObject * gobject) { GstSouphttpSrc *src = GST_SOUPHTTP_SRC (gobject); GST_DEBUG_OBJECT (src, "dispose"); g_free (src->location); src->location = NULL; g_free (src->user_agent); src->user_agent = NULL; g_free (src->iradio_name); src->iradio_name = NULL; g_free (src->iradio_genre); src->iradio_genre = NULL; g_free (src->iradio_url); src->iradio_url = NULL; g_free (src->iradio_title); src->iradio_title = NULL; if (src->icy_caps) { gst_caps_unref (src->icy_caps); src->icy_caps = NULL; } G_OBJECT_CLASS (parent_class)->dispose (gobject); } static void gst_souphttp_src_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstSouphttpSrc *src = GST_SOUPHTTP_SRC (object); switch (prop_id) { case PROP_LOCATION: { const gchar *location; location = g_value_get_string (value); if (location == NULL) { GST_WARNING ("location property cannot be NULL"); goto done; } if (!gst_souphttp_src_set_location (src, location)) { GST_WARNING ("badly formatted location"); goto done; } break; } case PROP_USER_AGENT: if (src->user_agent) g_free (src->user_agent); src->user_agent = g_value_dup_string (value); break; case PROP_IRADIO_MODE: src->iradio_mode = g_value_get_boolean (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } done: return; } static void gst_souphttp_src_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstSouphttpSrc *src = GST_SOUPHTTP_SRC (object); switch (prop_id) { case PROP_LOCATION: g_value_set_string (value, src->location); break; case PROP_USER_AGENT: g_value_set_string (value, src->user_agent); break; case PROP_IRADIO_MODE: g_value_set_boolean (value, src->iradio_mode); break; case PROP_IRADIO_NAME: g_value_set_string (value, src->iradio_name); break; case PROP_IRADIO_GENRE: g_value_set_string (value, src->iradio_genre); break; case PROP_IRADIO_URL: g_value_set_string (value, src->iradio_url); break; case PROP_IRADIO_TITLE: g_value_set_string (value, src->iradio_title); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static gchar * gst_souphttp_src_unicodify (const gchar * str) { const gchar *env_vars[] = { "GST_ICY_TAG_ENCODING", "GST_TAG_ENCODING", NULL }; 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); src->ret = GST_FLOW_CUSTOM_ERROR; src->outbuf = outbuf; 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); } 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) { return FALSE; } 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 void soup_got_headers (SoupMessage * msg, GstSouphttpSrc * src) { const char *value; GstTagList *tag_list; GST_DEBUG_OBJECT (src, "got headers"); /* Parse Content-Length. */ value = soup_message_get_header (msg->response_headers, "Content-Length"); if (value != NULL) { src->content_size = g_ascii_strtoull (value, NULL, 10); src->have_size = TRUE; GST_DEBUG_OBJECT (src, "size = %llu", src->content_size); gst_element_post_message (GST_ELEMENT (src), gst_message_new_duration (GST_OBJECT (src), GST_FORMAT_BYTES, src->content_size)); } /* Icecast stuff */ tag_list = gst_tag_list_new (); if ((value = soup_message_get_header (msg->response_headers, "icy-metaint")) != NULL) { gint icy_metaint = atoi (value); GST_DEBUG_OBJECT (src, "icy-metaint: %s (parsed: %d)", value, icy_metaint); if (icy_metaint > 0) src->icy_caps = gst_caps_new_simple ("application/x-icy", "metadata-interval", G_TYPE_INT, icy_metaint, NULL); } if ((value = soup_message_get_header (msg->response_headers, "icy-name")) != NULL) { g_free (src->iradio_name); src->iradio_name = gst_souphttp_src_unicodify (value); if (src->iradio_name) { g_object_notify (G_OBJECT (src), "iradio-name"); gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_ORGANIZATION, src->iradio_name, NULL); } } if ((value = soup_message_get_header (msg->response_headers, "icy-genre")) != NULL) { g_free (src->iradio_genre); src->iradio_genre = gst_souphttp_src_unicodify (value); if (src->iradio_genre) { g_object_notify (G_OBJECT (src), "iradio-genre"); gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_GENRE, src->iradio_genre, NULL); } } if ((value = soup_message_get_header (msg->response_headers, "icy-url")) != NULL) { g_free (src->iradio_url); src->iradio_url = gst_souphttp_src_unicodify (value); if (src->iradio_url) { g_object_notify (G_OBJECT (src), "iradio-url"); gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE, GST_TAG_LOCATION, src->iradio_url, NULL); } } if (!gst_tag_list_is_empty (tag_list)) { GST_DEBUG_OBJECT (src, "calling gst_element_found_tags with %" GST_PTR_FORMAT, tag_list); gst_element_found_tags (GST_ELEMENT_CAST (src), tag_list); } else { gst_tag_list_free (tag_list); } /* Handle HTTP errors. */ soup_parse_status (msg, src); } /* Have body. Signal EOS. */ static void soup_got_body (SoupMessage * msg, GstSouphttpSrc * src) { if (msg != src->msg) { GST_DEBUG_OBJECT (src, "got body, but not for current message"); 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); } /* Finished. Signal EOS. */ static void soup_finished (SoupMessage * msg, GstSouphttpSrc * src) { if (msg != src->msg) { GST_DEBUG_OBJECT (src, "finished, but not for current message"); return; } GST_DEBUG_OBJECT (src, "finished"); src->ret = GST_FLOW_UNEXPECTED; if (src->loop) g_main_loop_quit (src->loop); } static void soup_got_chunk (SoupMessage * msg, GstSouphttpSrc * src) { GstBaseSrc *basesrc; if (G_UNLIKELY (msg != src->msg)) { GST_DEBUG_OBJECT (src, "got chunk, but not for current message"); return; } basesrc = GST_BASE_SRC_CAST (src); GST_DEBUG_OBJECT (src, "got chunk of %d bytes", msg->response.length); /* Create the buffer. */ src->ret = gst_pad_alloc_buffer (GST_BASE_SRC_PAD (basesrc), basesrc->segment.last_stop, msg->response.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); src->read_position += msg->response.length; } g_main_loop_quit (src->loop); soup_message_io_pause (msg); } static void soup_response (SoupMessage * msg, gpointer user_data) { GstSouphttpSrc *src = (GstSouphttpSrc *) user_data; if (msg != src->msg) { GST_DEBUG_OBJECT (src, "got response %d: %s, but not for current message", msg->status_code, msg->reason_phrase); return; } GST_DEBUG_OBJECT (src, "got response %d: %s", msg->status_code, msg->reason_phrase); soup_parse_status (msg, src); g_main_loop_quit (src->loop); } static void soup_parse_status (SoupMessage * msg, GstSouphttpSrc * src) { if (SOUP_STATUS_IS_TRANSPORT_ERROR (msg->status_code)) { switch (msg->status_code) { case SOUP_STATUS_CANT_RESOLVE: GST_ELEMENT_ERROR (src, RESOURCE, NOT_FOUND, ("\"%s\": %s", src->location, msg->reason_phrase), ("libsoup status code %d", msg->status_code)); src->ret = GST_FLOW_ERROR; break; case SOUP_STATUS_CANT_RESOLVE_PROXY: GST_ELEMENT_ERROR (src, RESOURCE, NOT_FOUND, ("%s", msg->reason_phrase), ("libsoup status code %d", msg->status_code)); src->ret = GST_FLOW_ERROR; break; case SOUP_STATUS_CANT_CONNECT: case SOUP_STATUS_CANT_CONNECT_PROXY: case SOUP_STATUS_SSL_FAILED: GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, ("\"%s\": %s", src->location, msg->reason_phrase), ("libsoup status code %d", msg->status_code)); src->ret = GST_FLOW_ERROR; break; case SOUP_STATUS_IO_ERROR: case SOUP_STATUS_MALFORMED: GST_ELEMENT_ERROR (src, RESOURCE, READ, ("\"%s\": %s", src->location, msg->reason_phrase), ("libsoup status code %d", msg->status_code)); src->ret = GST_FLOW_ERROR; break; case SOUP_STATUS_CANCELLED: /* No error message when interrupted by program. */ break; } } else if (SOUP_STATUS_IS_CLIENT_ERROR (msg->status_code) || SOUP_STATUS_IS_SERVER_ERROR (msg->status_code)) { /* Report HTTP error. */ GST_ELEMENT_ERROR (src, RESOURCE, OPEN_READ, ("\"%s\": %s", src->location, msg->reason_phrase), ("%d %s", msg->status_code, msg->reason_phrase)); src->ret = GST_FLOW_ERROR; } } static void soup_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 guint gst_souphttp_src_uri_get_type (void) { return GST_URI_SRC; } static gchar ** gst_souphttp_src_uri_get_protocols (void) { static gchar *protocols[] = { "http", "https", NULL }; return protocols; } static const gchar * gst_souphttp_src_uri_get_uri (GstURIHandler * handler) { GstSouphttpSrc *src = GST_SOUPHTTP_SRC (handler); return src->location; } static gboolean gst_souphttp_src_uri_set_uri (GstURIHandler * handler, const gchar * uri) { GstSouphttpSrc *src = GST_SOUPHTTP_SRC (handler); return gst_souphttp_src_set_location (src, uri); } static void gst_souphttp_src_uri_handler_init (gpointer g_iface, gpointer iface_data) { GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface; iface->get_type = gst_souphttp_src_uri_get_type; iface->get_protocols = gst_souphttp_src_uri_get_protocols; iface->get_uri = gst_souphttp_src_uri_get_uri; iface->set_uri = gst_souphttp_src_uri_set_uri; } 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, GST_TYPE_SOUPHTTP_SRC); } GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, "soup", "libsoup http client src", plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)