mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-21 07:46:38 +00:00
048d703577
If the version of the curl library is recent enough to allow support for HTTP2 (i.e. CURL_VERSION_HTTP2 is defined) but does not actually have that feature enabled, the call to g_object_class_install_property() uses an incorrect default value for the "http-version" property. The default should be 1.1 if HTTP2 is not supported by libcurl or if not enabled by libcurl. https://bugzilla.gnome.org/show_bug.cgi?id=786049
1862 lines
63 KiB
C
1862 lines
63 KiB
C
/*
|
|
* GstCurlHttpSrc
|
|
* Copyright 2017 British Broadcasting Corporation - Research and Development
|
|
*
|
|
* Author: Sam Hurst <samuelh@rd.bbc.co.uk>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
* copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation
|
|
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
* and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
* DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* Alternatively, the contents of this file may be used under the
|
|
* GNU Lesser General Public License Version 2.1 (the "LGPL"), in
|
|
* which case the following provisions apply instead of the ones
|
|
* mentioned above:
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/**
|
|
* SECTION:element-curlhttpsrc
|
|
*
|
|
* This plugin reads data from a remote location specified by a URI, when the
|
|
* protocol is 'http' or 'https'.
|
|
*
|
|
* It is based on the cURL project (http://curl.haxx.se/) and is specifically
|
|
* designed to be also used with nghttp2 (http://nghttp2.org) to enable HTTP/2
|
|
* support for GStreamer. Your libcurl library MUST be compiled against nghttp2
|
|
* for HTTP/2 support for this functionality. HTTPS support is dependent on
|
|
* cURL being built with SSL support (OpenSSL/PolarSSL/NSS/GnuTLS).
|
|
*
|
|
* An HTTP proxy must be specified by URL.
|
|
* If the "http_proxy" environment variable is set, its value is used.
|
|
* The #GstCurlHttpSrc:proxy property can be used to override the default.
|
|
*
|
|
* <refsect2>
|
|
* <title>Example launch line</title>
|
|
* |[
|
|
* gst-launch-1.0 curlhttpsrc location=http://127.0.1.1/index.html ! fakesink dump=1
|
|
* ]| The above pipeline reads a web page from the local machine using HTTP and
|
|
* dumps it to stdout.
|
|
* |[
|
|
* gst-launch-1.0 playbin uri=http://rdmedia.bbc.co.uk/dash/testmpds/multiperiod/bbb.php
|
|
* ]| The above pipeline will start up a DASH streaming session from the given
|
|
* MPD file. This requires GStreamer to have been built with dashdemux from
|
|
* gst-plugins-bad.
|
|
* </refsect2>
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include "gstcurlhttpsrc.h"
|
|
#include "gstcurlqueue.h"
|
|
#include "gstcurldefaults.h"
|
|
|
|
GST_DEBUG_CATEGORY_STATIC (gst_curl_http_src_debug);
|
|
#define GST_CAT_DEFAULT gst_curl_http_src_debug
|
|
GST_DEBUG_CATEGORY_STATIC (gst_curl_loop_debug);
|
|
|
|
/*
|
|
* Make a source pad template to be able to kick out recv'd data
|
|
*/
|
|
static GstStaticPadTemplate srcpadtemplate = GST_STATIC_PAD_TEMPLATE ("src",
|
|
GST_PAD_SRC,
|
|
GST_PAD_ALWAYS,
|
|
GST_STATIC_CAPS_ANY);
|
|
|
|
/*
|
|
* Function Definitions
|
|
*/
|
|
/* Gstreamer generic element functions */
|
|
static void gst_curl_http_src_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec);
|
|
static void gst_curl_http_src_get_property (GObject * object, guint prop_id,
|
|
GValue * value, GParamSpec * pspec);
|
|
static void gst_curl_http_src_ref_multi (GstCurlHttpSrc * src);
|
|
static void gst_curl_http_src_unref_multi (GstCurlHttpSrc * src);
|
|
static void gst_curl_http_src_finalize (GObject * obj);
|
|
static GstFlowReturn gst_curl_http_src_create (GstPushSrc * psrc,
|
|
GstBuffer ** outbuf);
|
|
static GstFlowReturn gst_curl_http_src_handle_response (GstCurlHttpSrc * src);
|
|
static gboolean gst_curl_http_src_negotiate_caps (GstCurlHttpSrc * src);
|
|
static GstStateChangeReturn gst_curl_http_src_change_state (GstElement *
|
|
element, GstStateChange transition);
|
|
static void gst_curl_http_src_cleanup_instance (GstCurlHttpSrc * src);
|
|
static gboolean gst_curl_http_src_query (GstBaseSrc * bsrc, GstQuery * query);
|
|
static gboolean gst_curl_http_src_get_content_length (GstBaseSrc * bsrc,
|
|
guint64 * size);
|
|
static gboolean gst_curl_http_src_unlock (GstBaseSrc * bsrc);
|
|
static gboolean gst_curl_http_src_unlock_stop (GstBaseSrc * bsrc);
|
|
|
|
/* URI Handler functions */
|
|
static void gst_curl_http_src_uri_handler_init (gpointer g_iface,
|
|
gpointer iface_data);
|
|
static guint gst_curl_http_src_urihandler_get_type (GType type);
|
|
static const gchar *const *gst_curl_http_src_urihandler_get_protocols (GType
|
|
type);
|
|
static gchar *gst_curl_http_src_urihandler_get_uri (GstURIHandler * handler);
|
|
static gboolean gst_curl_http_src_urihandler_set_uri (GstURIHandler * handler,
|
|
const gchar * uri, GError ** error);
|
|
|
|
/* GstTask functions */
|
|
static void gst_curl_http_src_curl_multi_loop (gpointer thread_data);
|
|
static CURL *gst_curl_http_src_create_easy_handle (GstCurlHttpSrc * s);
|
|
static inline void gst_curl_http_src_destroy_easy_handle (GstCurlHttpSrc * src);
|
|
static size_t gst_curl_http_src_get_header (void *header, size_t size,
|
|
size_t nmemb, void *src);
|
|
static size_t gst_curl_http_src_get_chunks (void *chunk, size_t size,
|
|
size_t nmemb, void *src);
|
|
static void gst_curl_http_src_request_remove (GstCurlHttpSrc * src);
|
|
static char *gst_curl_http_src_strcasestr (const char *haystack,
|
|
const char *needle);
|
|
|
|
curl_version_info_data *gst_curl_http_src_curl_capabilities;
|
|
GstCurlHttpVersion pref_http_ver;
|
|
gchar *gst_curl_http_src_default_useragent;
|
|
|
|
#define GST_TYPE_CURL_HTTP_VERSION (gst_curl_http_version_get_type ())
|
|
static GType
|
|
gst_curl_http_version_get_type (void)
|
|
{
|
|
static GType gtype = 0;
|
|
|
|
if (!gtype) {
|
|
static const GEnumValue http_versions[] = {
|
|
{GSTCURL_HTTP_VERSION_1_0, "HTTP Version 1.0", "1.0"},
|
|
{GSTCURL_HTTP_VERSION_1_1, "HTTP Version 1.1", "1.1"},
|
|
#ifdef CURL_VERSION_HTTP2
|
|
{GSTCURL_HTTP_VERSION_2_0, "HTTP Version 2.0", "2.0"},
|
|
#endif
|
|
{0, NULL, NULL}
|
|
};
|
|
gtype = g_enum_register_static ("GstCurlHttpVersionType", http_versions);
|
|
}
|
|
return gtype;
|
|
}
|
|
|
|
#define gst_curl_http_src_parent_class parent_class
|
|
G_DEFINE_TYPE_WITH_CODE (GstCurlHttpSrc, gst_curl_http_src, GST_TYPE_PUSH_SRC,
|
|
G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER,
|
|
gst_curl_http_src_uri_handler_init));
|
|
|
|
static void
|
|
gst_curl_http_src_class_init (GstCurlHttpSrcClass * klass)
|
|
{
|
|
GObjectClass *gobject_class;
|
|
GstElementClass *gstelement_class;
|
|
GstBaseSrcClass *gstbasesrc_class;
|
|
GstPushSrcClass *gstpushsrc_class;
|
|
const gchar *http_env;
|
|
GstCurlHttpVersion default_http_version;
|
|
|
|
gobject_class = (GObjectClass *) klass;
|
|
gstelement_class = (GstElementClass *) klass;
|
|
gstbasesrc_class = (GstBaseSrcClass *) klass;
|
|
gstpushsrc_class = (GstPushSrcClass *) klass;
|
|
|
|
GST_DEBUG_CATEGORY_INIT (gst_curl_http_src_debug, "curlhttpsrc",
|
|
0, "UriHandler for libcURL");
|
|
|
|
GST_INFO_OBJECT (klass, "class_init started!");
|
|
|
|
gstelement_class->change_state =
|
|
GST_DEBUG_FUNCPTR (gst_curl_http_src_change_state);
|
|
gstpushsrc_class->create = GST_DEBUG_FUNCPTR (gst_curl_http_src_create);
|
|
gstbasesrc_class->query = GST_DEBUG_FUNCPTR (gst_curl_http_src_query);
|
|
gstbasesrc_class->get_size =
|
|
GST_DEBUG_FUNCPTR (gst_curl_http_src_get_content_length);
|
|
gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR (gst_curl_http_src_unlock);
|
|
gstbasesrc_class->unlock_stop =
|
|
GST_DEBUG_FUNCPTR (gst_curl_http_src_unlock_stop);
|
|
|
|
gst_element_class_add_pad_template (gstelement_class,
|
|
gst_static_pad_template_get (&srcpadtemplate));
|
|
|
|
gst_curl_http_src_curl_capabilities = curl_version_info (CURLVERSION_NOW);
|
|
#ifdef CURL_VERSION_HTTP2
|
|
if (gst_curl_http_src_curl_capabilities->features & CURL_VERSION_HTTP2) {
|
|
default_http_version = GSTCURL_HTTP_VERSION_2_0;
|
|
} else
|
|
#endif
|
|
default_http_version = GSTCURL_HTTP_VERSION_1_1;
|
|
|
|
http_env = g_getenv ("GST_CURL_HTTP_VER");
|
|
if (http_env != NULL) {
|
|
GST_INFO_OBJECT (klass, "Seen env var GST_CURL_HTTP_VER with value %s",
|
|
http_env);
|
|
if (!strcmp (http_env, "1.0")) {
|
|
pref_http_ver = GSTCURL_HTTP_VERSION_1_0;
|
|
} else if (!strcmp (http_env, "1.1")) {
|
|
pref_http_ver = GSTCURL_HTTP_VERSION_1_1;
|
|
} else if (!strcmp (http_env, "2.0")) {
|
|
#ifdef CURL_VERSION_HTTP2
|
|
if (gst_curl_http_src_curl_capabilities->features & CURL_VERSION_HTTP2) {
|
|
pref_http_ver = GSTCURL_HTTP_VERSION_2_0;
|
|
} else {
|
|
goto unsupported_http_version;
|
|
}
|
|
#endif
|
|
} else {
|
|
unsupported_http_version:
|
|
GST_WARNING_OBJECT (klass,
|
|
"Unsupported HTTP version: %s. Fallback to default", http_env);
|
|
pref_http_ver = default_http_version;
|
|
}
|
|
} else {
|
|
pref_http_ver = default_http_version;
|
|
}
|
|
|
|
gst_curl_http_src_default_useragent =
|
|
g_strdup_printf ("GStreamer curlhttpsrc libcurl/%s",
|
|
gst_curl_http_src_curl_capabilities->version);
|
|
|
|
gobject_class->set_property = gst_curl_http_src_set_property;
|
|
gobject_class->get_property = gst_curl_http_src_get_property;
|
|
gobject_class->finalize = gst_curl_http_src_finalize;
|
|
|
|
g_object_class_install_property (gobject_class, PROP_URI,
|
|
g_param_spec_string ("location", "Location", "URI of resource to read",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_URL,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_USERNAME,
|
|
g_param_spec_string ("user-id", "user-id",
|
|
"HTTP location URI user id for authentication",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_USERNAME,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_PASSWORD,
|
|
g_param_spec_string ("user-pw", "user-pw",
|
|
"HTTP location URI password for authentication",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_PASSWORD,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_PROXYURI,
|
|
g_param_spec_string ("proxy", "Proxy", "URI of HTTP proxy server",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_PROXY,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_PROXYUSERNAME,
|
|
g_param_spec_string ("proxy-id", "proxy-id",
|
|
"HTTP proxy URI user id for authentication",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_PROXYUSERNAME,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_PROXYPASSWORD,
|
|
g_param_spec_string ("proxy-pw", "proxy-pw",
|
|
"HTTP proxy URI password for authentication",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_PROXYPASSWORD,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_COOKIES,
|
|
g_param_spec_boxed ("cookies", "Cookies", "List of HTTP Cookies",
|
|
G_TYPE_STRV, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_USERAGENT,
|
|
g_param_spec_string ("user-agent", "User-Agent",
|
|
"URI of resource requested", GSTCURL_HANDLE_DEFAULT_CURLOPT_USERAGENT,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_COMPRESS,
|
|
g_param_spec_boolean ("compress", "Compress",
|
|
"Allow compressed content encodings",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_ACCEPT_ENCODING,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_REDIRECT,
|
|
g_param_spec_boolean ("automatic-redirect", "automatic-redirect",
|
|
"Allow HTTP Redirections (HTTP Status Code 300 series)",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_FOLLOWLOCATION,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_MAXREDIRECT,
|
|
g_param_spec_int ("max-redirect", "Max-Redirect",
|
|
"Maximum number of permitted redirections. -1 is unlimited.",
|
|
GSTCURL_HANDLE_MIN_CURLOPT_MAXREDIRS,
|
|
GSTCURL_HANDLE_MAX_CURLOPT_MAXREDIRS,
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_MAXREDIRS,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_KEEPALIVE,
|
|
g_param_spec_boolean ("keep-alive", "Keep-Alive",
|
|
"Toggle keep-alive for connection reuse.",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_TCP_KEEPALIVE,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_TIMEOUT,
|
|
g_param_spec_int ("timeout", "Timeout",
|
|
"Value in seconds before timeout a blocking request (0 = no timeout)",
|
|
GSTCURL_HANDLE_MIN_CURLOPT_TIMEOUT,
|
|
GSTCURL_HANDLE_MAX_CURLOPT_TIMEOUT,
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_TIMEOUT,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_HEADERS,
|
|
g_param_spec_boxed ("extra-headers", "Extra Headers",
|
|
"Extra headers to append to the HTTP request",
|
|
GST_TYPE_STRUCTURE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_STRICT_SSL,
|
|
g_param_spec_boolean ("ssl-strict", "SSL Strict",
|
|
"Strict SSL certificate checking",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_SSL_VERIFYPEER,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_SSL_CA_FILE,
|
|
g_param_spec_string ("ssl-ca-file", "SSL CA File",
|
|
"Location of an SSL CA file to use for checking SSL certificates",
|
|
GSTCURL_HANDLE_DEFAULT_CURLOPT_CAINFO,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_RETRIES,
|
|
g_param_spec_int ("retries", "Retries",
|
|
"Maximum number of retries until giving up (-1=infinite)",
|
|
GSTCURL_HANDLE_MIN_RETRIES, GSTCURL_HANDLE_MAX_RETRIES,
|
|
GSTCURL_HANDLE_DEFAULT_RETRIES,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_CONNECTIONMAXTIME,
|
|
g_param_spec_uint ("max-connection-time", "Max-Connection-Time",
|
|
"Maximum amount of time to keep-alive HTTP connections",
|
|
GSTCURL_MIN_CONNECTION_TIME, GSTCURL_MAX_CONNECTION_TIME,
|
|
GSTCURL_DEFAULT_CONNECTION_TIME,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_MAXCONCURRENT_SERVER,
|
|
g_param_spec_uint ("max-connections-per-server",
|
|
"Max-Connections-Per-Server",
|
|
"Maximum number of connections allowed per server for HTTP/1.x",
|
|
GSTCURL_MIN_CONNECTIONS_SERVER, GSTCURL_MAX_CONNECTIONS_SERVER,
|
|
GSTCURL_DEFAULT_CONNECTIONS_SERVER,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_MAXCONCURRENT_PROXY,
|
|
g_param_spec_uint ("max-connections-per-proxy",
|
|
"Max-Connections-Per-Proxy",
|
|
"Maximum number of concurrent connections allowed per proxy for HTTP/1.x",
|
|
GSTCURL_MIN_CONNECTIONS_PROXY, GSTCURL_MAX_CONNECTIONS_PROXY,
|
|
GSTCURL_DEFAULT_CONNECTIONS_PROXY,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_MAXCONCURRENT_GLOBAL,
|
|
g_param_spec_uint ("max-connections", "Max-Connections",
|
|
"Maximum number of concurrent connections allowed for HTTP/1.x",
|
|
GSTCURL_MIN_CONNECTIONS_GLOBAL, GSTCURL_MAX_CONNECTIONS_GLOBAL,
|
|
GSTCURL_DEFAULT_CONNECTIONS_GLOBAL,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
g_object_class_install_property (gobject_class, PROP_HTTPVERSION,
|
|
g_param_spec_enum ("http-version", "HTTP-Version",
|
|
"The preferred HTTP protocol version",
|
|
GST_TYPE_CURL_HTTP_VERSION, pref_http_ver,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
/* Add a debugging task so it's easier to debug in the Multi worker thread */
|
|
GST_DEBUG_CATEGORY_INIT (gst_curl_loop_debug, "curl_multi_loop", 0,
|
|
"libcURL loop thread debugging");
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
gst_debug_log (gst_curl_loop_debug, GST_LEVEL_INFO, __FILE__, __func__,
|
|
__LINE__, NULL, "Testing the curl_multi_loop debugging prints");
|
|
#endif
|
|
|
|
g_mutex_init (&klass->multi_task_context.mutex);
|
|
g_cond_init (&klass->multi_task_context.signal);
|
|
g_rec_mutex_init (&klass->multi_task_context.task_rec_mutex);
|
|
|
|
gst_element_class_set_static_metadata (gstelement_class,
|
|
"HTTP Client Source using libcURL",
|
|
"Source/Network",
|
|
"Receiver data as a client over a network via HTTP using cURL",
|
|
"Sam Hurst <samuelh@rd.bbc.co.uk>");
|
|
}
|
|
|
|
static void
|
|
gst_curl_http_src_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
GstCurlHttpSrc *source = GST_CURLHTTPSRC (object);
|
|
GSTCURL_FUNCTION_ENTRY (source);
|
|
|
|
switch (prop_id) {
|
|
case PROP_URI:
|
|
g_free (source->uri);
|
|
source->uri = g_value_dup_string (value);
|
|
break;
|
|
case PROP_USERNAME:
|
|
g_free (source->username);
|
|
source->username = g_value_dup_string (value);
|
|
break;
|
|
case PROP_PASSWORD:
|
|
g_free (source->password);
|
|
source->password = g_value_dup_string (value);
|
|
break;
|
|
case PROP_PROXYURI:
|
|
g_free (source->proxy_uri);
|
|
source->proxy_uri = g_value_dup_string (value);
|
|
break;
|
|
case PROP_PROXYUSERNAME:
|
|
g_free (source->proxy_user);
|
|
source->proxy_user = g_value_dup_string (value);
|
|
break;
|
|
case PROP_PROXYPASSWORD:
|
|
g_free (source->proxy_pass);
|
|
source->proxy_pass = g_value_dup_string (value);
|
|
break;
|
|
case PROP_COOKIES:
|
|
g_strfreev (source->cookies);
|
|
source->cookies = g_strdupv (g_value_get_boxed (value));
|
|
source->number_cookies = g_strv_length (source->cookies);
|
|
break;
|
|
case PROP_USERAGENT:
|
|
g_free (source->user_agent);
|
|
source->user_agent = g_value_dup_string (value);
|
|
break;
|
|
case PROP_HEADERS:
|
|
{
|
|
const GstStructure *s = gst_value_get_structure (value);
|
|
if (source->request_headers)
|
|
gst_structure_free (source->request_headers);
|
|
source->request_headers = s ? gst_structure_copy (s) : NULL;
|
|
}
|
|
break;
|
|
case PROP_COMPRESS:
|
|
source->accept_compressed_encodings = g_value_get_boolean (value);
|
|
break;
|
|
case PROP_REDIRECT:
|
|
source->allow_3xx_redirect = g_value_get_boolean (value);
|
|
break;
|
|
case PROP_MAXREDIRECT:
|
|
source->max_3xx_redirects = g_value_get_int (value);
|
|
break;
|
|
case PROP_KEEPALIVE:
|
|
source->keep_alive = g_value_get_boolean (value);
|
|
break;
|
|
case PROP_TIMEOUT:
|
|
source->timeout_secs = g_value_get_int (value);
|
|
break;
|
|
case PROP_STRICT_SSL:
|
|
source->strict_ssl = g_value_get_boolean (value);
|
|
break;
|
|
case PROP_SSL_CA_FILE:
|
|
source->custom_ca_file = g_value_dup_string (value);
|
|
break;
|
|
case PROP_RETRIES:
|
|
source->total_retries = g_value_get_int (value);
|
|
break;
|
|
case PROP_CONNECTIONMAXTIME:
|
|
source->max_connection_time = g_value_get_uint (value);
|
|
break;
|
|
case PROP_MAXCONCURRENT_SERVER:
|
|
source->max_conns_per_server = g_value_get_uint (value);
|
|
break;
|
|
case PROP_MAXCONCURRENT_PROXY:
|
|
source->max_conns_per_proxy = g_value_get_uint (value);
|
|
break;
|
|
case PROP_MAXCONCURRENT_GLOBAL:
|
|
source->max_conns_global = g_value_get_uint (value);
|
|
break;
|
|
case PROP_HTTPVERSION:
|
|
source->preferred_http_version = g_value_get_enum (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
GSTCURL_FUNCTION_EXIT (source);
|
|
}
|
|
|
|
static void
|
|
gst_curl_http_src_get_property (GObject * object, guint prop_id,
|
|
GValue * value, GParamSpec * pspec)
|
|
{
|
|
GstCurlHttpSrc *source = GST_CURLHTTPSRC (object);
|
|
GSTCURL_FUNCTION_ENTRY (source);
|
|
|
|
switch (prop_id) {
|
|
case PROP_URI:
|
|
g_value_set_string (value, source->uri);
|
|
break;
|
|
case PROP_USERNAME:
|
|
g_value_set_string (value, source->username);
|
|
break;
|
|
case PROP_PASSWORD:
|
|
g_value_set_string (value, source->password);
|
|
break;
|
|
case PROP_PROXYURI:
|
|
g_value_set_string (value, source->proxy_uri);
|
|
break;
|
|
case PROP_PROXYUSERNAME:
|
|
g_value_set_string (value, source->proxy_user);
|
|
break;
|
|
case PROP_PROXYPASSWORD:
|
|
g_value_set_string (value, source->proxy_pass);
|
|
break;
|
|
case PROP_COOKIES:
|
|
g_value_set_boxed (value, source->cookies);
|
|
break;
|
|
case PROP_USERAGENT:
|
|
g_value_set_string (value, source->user_agent);
|
|
break;
|
|
case PROP_HEADERS:
|
|
gst_value_set_structure (value, source->request_headers);
|
|
break;
|
|
case PROP_COMPRESS:
|
|
g_value_set_boolean (value, source->accept_compressed_encodings);
|
|
break;
|
|
case PROP_REDIRECT:
|
|
g_value_set_boolean (value, source->allow_3xx_redirect);
|
|
break;
|
|
case PROP_MAXREDIRECT:
|
|
g_value_set_int (value, source->max_3xx_redirects);
|
|
break;
|
|
case PROP_KEEPALIVE:
|
|
g_value_set_boolean (value, source->keep_alive);
|
|
break;
|
|
case PROP_TIMEOUT:
|
|
g_value_set_int (value, source->timeout_secs);
|
|
break;
|
|
case PROP_STRICT_SSL:
|
|
g_value_set_boolean (value, source->strict_ssl);
|
|
break;
|
|
case PROP_SSL_CA_FILE:
|
|
g_value_set_string (value, source->custom_ca_file);
|
|
break;
|
|
case PROP_RETRIES:
|
|
g_value_set_int (value, source->total_retries);
|
|
break;
|
|
case PROP_CONNECTIONMAXTIME:
|
|
g_value_set_uint (value, source->max_connection_time);
|
|
break;
|
|
case PROP_MAXCONCURRENT_SERVER:
|
|
g_value_set_uint (value, source->max_conns_per_server);
|
|
break;
|
|
case PROP_MAXCONCURRENT_PROXY:
|
|
g_value_set_uint (value, source->max_conns_per_proxy);
|
|
break;
|
|
case PROP_MAXCONCURRENT_GLOBAL:
|
|
g_value_set_uint (value, source->max_conns_global);
|
|
break;
|
|
case PROP_HTTPVERSION:
|
|
g_value_set_enum (value, source->preferred_http_version);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
GSTCURL_FUNCTION_EXIT (source);
|
|
}
|
|
|
|
static void
|
|
gst_curl_http_src_init (GstCurlHttpSrc * source)
|
|
{
|
|
GSTCURL_FUNCTION_ENTRY (source);
|
|
|
|
/* Assume everything is already free'd */
|
|
source->uri = NULL;
|
|
source->redirect_uri = NULL;
|
|
source->username = GSTCURL_HANDLE_DEFAULT_CURLOPT_USERNAME;
|
|
source->password = GSTCURL_HANDLE_DEFAULT_CURLOPT_PASSWORD;
|
|
source->proxy_uri = NULL;
|
|
source->proxy_user = NULL;
|
|
source->proxy_pass = NULL;
|
|
source->cookies = NULL;
|
|
source->user_agent = GSTCURL_HANDLE_DEFAULT_CURLOPT_USERAGENT;
|
|
source->number_cookies = 0;
|
|
source->request_headers = NULL;
|
|
source->allow_3xx_redirect = GSTCURL_HANDLE_DEFAULT_CURLOPT_FOLLOWLOCATION;
|
|
source->max_3xx_redirects = GSTCURL_HANDLE_DEFAULT_CURLOPT_MAXREDIRS;
|
|
source->keep_alive = GSTCURL_HANDLE_DEFAULT_CURLOPT_TCP_KEEPALIVE;
|
|
source->timeout_secs = GSTCURL_HANDLE_DEFAULT_CURLOPT_TIMEOUT;
|
|
source->max_connection_time = GSTCURL_DEFAULT_CONNECTION_TIME;
|
|
source->max_conns_per_server = GSTCURL_DEFAULT_CONNECTIONS_SERVER;
|
|
source->max_conns_per_proxy = GSTCURL_DEFAULT_CONNECTIONS_PROXY;
|
|
source->max_conns_global = GSTCURL_DEFAULT_CONNECTIONS_GLOBAL;
|
|
source->strict_ssl = GSTCURL_HANDLE_DEFAULT_CURLOPT_SSL_VERIFYPEER;
|
|
source->custom_ca_file = NULL;
|
|
source->preferred_http_version = pref_http_ver;
|
|
source->total_retries = GSTCURL_HANDLE_DEFAULT_RETRIES;
|
|
source->retries_remaining = source->total_retries;
|
|
source->slist = NULL;
|
|
|
|
gst_caps_replace (&source->caps, NULL);
|
|
gst_base_src_set_automatic_eos (GST_BASE_SRC (source), FALSE);
|
|
|
|
source->proxy_uri = g_strdup (g_getenv ("http_proxy"));
|
|
source->no_proxy_list = g_strdup (g_getenv ("no_proxy"));
|
|
|
|
g_mutex_init (&source->uri_mutex);
|
|
g_mutex_init (&source->buffer_mutex);
|
|
g_cond_init (&source->signal);
|
|
|
|
source->buffer = NULL;
|
|
source->buffer_len = 0;
|
|
source->state = GSTCURL_NONE;
|
|
source->pending_state = GSTCURL_NONE;
|
|
source->status_code = 0;
|
|
|
|
source->http_headers = NULL;
|
|
source->hdrs_updated = FALSE;
|
|
|
|
source->curl_result = CURLE_OK;
|
|
|
|
GSTCURL_FUNCTION_EXIT (source);
|
|
}
|
|
|
|
/*
|
|
* Check if the Curl multi loop has been started. If not, initialise it and
|
|
* start it running. If it is already running, increment the refcount.
|
|
*/
|
|
static void
|
|
gst_curl_http_src_ref_multi (GstCurlHttpSrc * src)
|
|
{
|
|
GstCurlHttpSrcClass *klass;
|
|
|
|
GSTCURL_FUNCTION_ENTRY (src);
|
|
|
|
/*klass = (GstCurlHttpSrcClass) g_type_class_peek_parent (src); */
|
|
klass = G_TYPE_INSTANCE_GET_CLASS (src, GST_TYPE_CURL_HTTP_SRC,
|
|
GstCurlHttpSrcClass);
|
|
|
|
g_mutex_lock (&klass->multi_task_context.mutex);
|
|
if (klass->multi_task_context.refcount == 0) {
|
|
/* Set up various in-task properties */
|
|
|
|
/* NULL is treated as the start of the list, no need to allocate. */
|
|
klass->multi_task_context.queue = NULL;
|
|
|
|
/* set up curl */
|
|
klass->multi_task_context.multi_handle = curl_multi_init ();
|
|
|
|
curl_multi_setopt (klass->multi_task_context.multi_handle,
|
|
CURLMOPT_PIPELINING, 1);
|
|
#ifdef CURLMOPT_MAX_HOST_CONNECTIONS
|
|
curl_multi_setopt (klass->multi_task_context.multi_handle,
|
|
CURLMOPT_MAX_HOST_CONNECTIONS, 1);
|
|
#endif
|
|
|
|
/* Start the thread */
|
|
klass->multi_task_context.task = gst_task_new (
|
|
(GstTaskFunction) gst_curl_http_src_curl_multi_loop,
|
|
(gpointer) & klass->multi_task_context, NULL);
|
|
gst_task_set_lock (klass->multi_task_context.task,
|
|
&klass->multi_task_context.task_rec_mutex);
|
|
if (gst_task_start (klass->multi_task_context.task) == FALSE) {
|
|
/*
|
|
* This is a pretty critical failure and is not recoverable, so commit
|
|
* sudoku and run away.
|
|
*/
|
|
GSTCURL_ERROR_PRINT ("Couldn't start curl_multi task! Aborting.");
|
|
abort ();
|
|
}
|
|
GSTCURL_INFO_PRINT ("Curl multi loop has been correctly initialised!");
|
|
}
|
|
klass->multi_task_context.refcount++;
|
|
g_mutex_unlock (&klass->multi_task_context.mutex);
|
|
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
}
|
|
|
|
/*
|
|
* Decrement the reference count on the curl multi loop. If this is called by
|
|
* the last instance to hold a reference, shut down the worker. (Otherwise
|
|
* GStreamer can't close down with a thread still running). Also offers the
|
|
* "force_all" boolean parameter, which if TRUE removes all references and shuts
|
|
* down.
|
|
*/
|
|
static void
|
|
gst_curl_http_src_unref_multi (GstCurlHttpSrc * src)
|
|
{
|
|
GstCurlHttpSrcClass *klass;
|
|
|
|
GSTCURL_FUNCTION_ENTRY (src);
|
|
|
|
klass = G_TYPE_INSTANCE_GET_CLASS (src, GST_TYPE_CURL_HTTP_SRC,
|
|
GstCurlHttpSrcClass);
|
|
|
|
g_mutex_lock (&klass->multi_task_context.mutex);
|
|
klass->multi_task_context.refcount--;
|
|
GST_INFO_OBJECT (src, "Closing instance, worker thread refcount is now %u",
|
|
klass->multi_task_context.refcount);
|
|
|
|
if (klass->multi_task_context.refcount <= 0) {
|
|
/* Everything's done! Clean up. */
|
|
gst_task_pause (klass->multi_task_context.task);
|
|
klass->multi_task_context.state = GSTCURL_MULTI_LOOP_STATE_STOP;
|
|
g_cond_signal (&klass->multi_task_context.signal);
|
|
g_mutex_unlock (&klass->multi_task_context.mutex);
|
|
gst_task_join (klass->multi_task_context.task);
|
|
} else {
|
|
g_mutex_unlock (&klass->multi_task_context.mutex);
|
|
}
|
|
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
}
|
|
|
|
static void
|
|
gst_curl_http_src_finalize (GObject * obj)
|
|
{
|
|
GstCurlHttpSrc *src = GST_CURLHTTPSRC (obj);
|
|
|
|
GSTCURL_FUNCTION_ENTRY (src);
|
|
|
|
/* Cleanup all memory allocated */
|
|
gst_curl_http_src_cleanup_instance (src);
|
|
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
}
|
|
|
|
/*
|
|
* Do the transfer. If the transfer hasn't begun yet, start a new curl handle
|
|
* and pass it to the multi queue to be operated on. Then wait for any blocks
|
|
* of data and push them to the source pad.
|
|
*/
|
|
static GstFlowReturn
|
|
gst_curl_http_src_create (GstPushSrc * psrc, GstBuffer ** outbuf)
|
|
{
|
|
GstFlowReturn ret;
|
|
GstCurlHttpSrc *src = GST_CURLHTTPSRC (psrc);
|
|
GstCurlHttpSrcClass *klass;
|
|
GstStructure *empty_headers;
|
|
|
|
klass = G_TYPE_INSTANCE_GET_CLASS (src, GST_TYPE_CURL_HTTP_SRC,
|
|
GstCurlHttpSrcClass);
|
|
|
|
GSTCURL_FUNCTION_ENTRY (src);
|
|
ret = GST_FLOW_OK;
|
|
|
|
g_mutex_lock (&src->buffer_mutex);
|
|
if (src->state == GSTCURL_UNLOCK) {
|
|
ret = GST_FLOW_FLUSHING;
|
|
goto escape;
|
|
}
|
|
|
|
retry:
|
|
if (!src->transfer_begun) {
|
|
GST_DEBUG_OBJECT (src, "Starting new request for URI %s", src->uri);
|
|
/* Create the Easy Handle and set up the session. */
|
|
src->curl_handle = gst_curl_http_src_create_easy_handle (src);
|
|
if (src->curl_handle == NULL) {
|
|
ret = GST_FLOW_ERROR;
|
|
goto escape;
|
|
}
|
|
|
|
g_mutex_lock (&klass->multi_task_context.mutex);
|
|
|
|
if (gst_curl_http_src_add_queue_item (&klass->multi_task_context.queue, src)
|
|
== FALSE) {
|
|
GST_ERROR_OBJECT (src, "Couldn't create new queue item! Aborting...");
|
|
ret = GST_FLOW_ERROR;
|
|
goto escape;
|
|
}
|
|
|
|
/* Signal the worker thread */
|
|
klass->multi_task_context.state = GSTCURL_MULTI_LOOP_STATE_QUEUE_EVENT;
|
|
g_cond_signal (&klass->multi_task_context.signal);
|
|
g_mutex_unlock (&klass->multi_task_context.mutex);
|
|
|
|
src->state = GSTCURL_OK;
|
|
src->transfer_begun = TRUE;
|
|
src->data_received = FALSE;
|
|
|
|
GST_DEBUG_OBJECT (src, "Submitted request for URI %s to curl", src->uri);
|
|
|
|
empty_headers = gst_structure_new_empty (RESPONSE_HEADERS_NAME);
|
|
src->http_headers = gst_structure_new (HTTP_HEADERS_NAME,
|
|
URI_NAME, G_TYPE_STRING, src->uri,
|
|
REQUEST_HEADERS_NAME, GST_TYPE_STRUCTURE, src->request_headers,
|
|
RESPONSE_HEADERS_NAME, GST_TYPE_STRUCTURE, empty_headers, NULL);
|
|
gst_structure_free (empty_headers);
|
|
GST_INFO_OBJECT (src, "Created a new headers object");
|
|
}
|
|
|
|
/* Wait for data to become available, then punt it downstream */
|
|
while ((src->buffer_len == 0) && (src->state == GSTCURL_OK)) {
|
|
g_cond_wait (&src->signal, &src->buffer_mutex);
|
|
}
|
|
|
|
if (src->state == GSTCURL_UNLOCK) {
|
|
if (src->buffer_len > 0) {
|
|
g_free (src->buffer);
|
|
src->buffer = NULL;
|
|
src->buffer_len = 0;
|
|
}
|
|
ret = GST_FLOW_FLUSHING;
|
|
goto escape;
|
|
}
|
|
|
|
ret = gst_curl_http_src_handle_response (src);
|
|
switch (ret) {
|
|
case GST_FLOW_ERROR:
|
|
goto escape; /* Don't attempt a retry, just bomb out */
|
|
case GST_FLOW_CUSTOM_ERROR:
|
|
if (src->data_received == TRUE) {
|
|
/*
|
|
* If data has already been received, we can't recall previously sent
|
|
* buffers so don't attempt a retry in this case.
|
|
*
|
|
* TODO: Remember the position we got to, and make a range request for
|
|
* the resource without the bit we've already received?
|
|
*/
|
|
GST_WARNING_OBJECT (src,
|
|
"Failed mid-transfer, can't continue for URI %s", src->uri);
|
|
ret = GST_FLOW_ERROR;
|
|
goto escape;
|
|
}
|
|
src->retries_remaining--;
|
|
if (src->retries_remaining == 0) {
|
|
GST_WARNING_OBJECT (src, "Out of retries for URI %s", src->uri);
|
|
ret = GST_FLOW_ERROR; /* Don't attempt a retry, just bomb out */
|
|
goto escape;
|
|
}
|
|
GST_INFO_OBJECT (src, "Attempting retry for URI %s", src->uri);
|
|
src->state = GSTCURL_NONE;
|
|
src->transfer_begun = FALSE;
|
|
src->status_code = 0;
|
|
src->hdrs_updated = FALSE;
|
|
if (src->http_headers != NULL) {
|
|
gst_structure_free (src->http_headers);
|
|
src->http_headers = NULL;
|
|
GST_INFO_OBJECT (src, "NULL'd the headers");
|
|
}
|
|
gst_curl_http_src_destroy_easy_handle (src);
|
|
g_mutex_unlock (&src->buffer_mutex);
|
|
goto retry; /* Attempt a retry! */
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (((src->state == GSTCURL_OK) || (src->state == GSTCURL_DONE)) &&
|
|
(src->buffer_len > 0)) {
|
|
|
|
GST_DEBUG_OBJECT (src, "Pushing %u bytes of transfer for URI %s to pad",
|
|
src->buffer_len, src->uri);
|
|
*outbuf = gst_buffer_new_allocate (NULL, src->buffer_len, NULL);
|
|
gst_buffer_fill (*outbuf, 0, src->buffer, src->buffer_len);
|
|
|
|
g_free (src->buffer);
|
|
src->buffer = NULL;
|
|
src->buffer_len = 0;
|
|
src->data_received = TRUE;
|
|
|
|
/* ret should still be GST_FLOW_OK */
|
|
} else if ((src->state == GSTCURL_DONE) && (src->buffer_len == 0)) {
|
|
GST_INFO_OBJECT (src, "Full body received, signalling EOS for URI %s.",
|
|
src->uri);
|
|
src->state = GSTCURL_NONE;
|
|
src->transfer_begun = FALSE;
|
|
src->status_code = 0;
|
|
src->hdrs_updated = FALSE;
|
|
gst_curl_http_src_destroy_easy_handle (src);
|
|
ret = GST_FLOW_EOS;
|
|
} else {
|
|
switch (src->state) {
|
|
case GSTCURL_NONE:
|
|
GST_WARNING_OBJECT (src, "Got unexpected GSTCURL_NONE state!");
|
|
break;
|
|
case GSTCURL_REMOVED:
|
|
GST_WARNING_OBJECT (src, "Transfer got removed from the curl queue");
|
|
ret = GST_FLOW_EOS;
|
|
break;
|
|
case GSTCURL_BAD_QUEUE_REQUEST:
|
|
GST_ERROR_OBJECT (src, "Bad Queue Request!");
|
|
ret = GST_FLOW_ERROR;
|
|
break;
|
|
case GSTCURL_TOTAL_ERROR:
|
|
GST_ERROR_OBJECT (src, "Critical, unrecoverable error!");
|
|
ret = GST_FLOW_ERROR;
|
|
break;
|
|
case GSTCURL_PIPELINE_NULL:
|
|
GST_ERROR_OBJECT (src, "Pipeline null");
|
|
break;
|
|
default:
|
|
GST_ERROR_OBJECT (src, "Unknown state of %u", src->state);
|
|
}
|
|
}
|
|
|
|
escape:
|
|
g_mutex_unlock (&src->buffer_mutex);
|
|
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Convert header from a GstStructure type to a curl_slist type that curl will
|
|
* understand.
|
|
*/
|
|
static gboolean
|
|
_headers_to_curl_slist (GQuark field_id, const GValue * value, gpointer ptr)
|
|
{
|
|
gchar *field;
|
|
struct curl_slist **p_slist = ptr;
|
|
|
|
field = g_strdup_printf ("%s: %s", g_quark_to_string (field_id),
|
|
g_value_get_string (value));
|
|
|
|
*p_slist = curl_slist_append (*p_slist, field);
|
|
|
|
g_free (field);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* From the data in the queue element s, create a CURL easy handle and populate
|
|
* options with the URL, proxy data, login options, cookies,
|
|
*/
|
|
static CURL *
|
|
gst_curl_http_src_create_easy_handle (GstCurlHttpSrc * s)
|
|
{
|
|
CURL *handle;
|
|
gint i;
|
|
GSTCURL_FUNCTION_ENTRY (s);
|
|
|
|
handle = curl_easy_init ();
|
|
if (handle == NULL) {
|
|
GST_ERROR_OBJECT (s, "Couldn't init a curl easy handle!");
|
|
return NULL;
|
|
}
|
|
GST_INFO_OBJECT (s, "Creating a new handle for URI %s", s->uri);
|
|
|
|
/* This is mandatory and yet not default option, so if this is NULL
|
|
* then something very bad is going on. */
|
|
if (s->uri == NULL) {
|
|
GST_ERROR_OBJECT (s, "No URI for curl!");
|
|
return NULL;
|
|
}
|
|
gst_curl_setopt_str (s, handle, CURLOPT_URL, s->uri);
|
|
|
|
gst_curl_setopt_str (s, handle, CURLOPT_USERNAME, s->username);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_PASSWORD, s->password);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_PROXY, s->proxy_uri);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_NOPROXY, s->no_proxy_list);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_PROXYUSERNAME, s->proxy_user);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_PROXYPASSWORD, s->proxy_pass);
|
|
|
|
for (i = 0; i < s->number_cookies; i++) {
|
|
gst_curl_setopt_str (s, handle, CURLOPT_COOKIELIST, s->cookies[i]);
|
|
}
|
|
|
|
/* curl_slist_append dynamically allocates memory, but I need to free it */
|
|
if (s->request_headers != NULL) {
|
|
gst_structure_foreach (s->request_headers, _headers_to_curl_slist,
|
|
&s->slist);
|
|
if (curl_easy_setopt (handle, CURLOPT_HTTPHEADER, s->slist) != CURLE_OK) {
|
|
GST_WARNING_OBJECT (s, "Failed to set HTTP headers!");
|
|
}
|
|
}
|
|
|
|
gst_curl_setopt_str_default (s, handle, CURLOPT_USERAGENT, s->user_agent);
|
|
|
|
/*
|
|
* Unlike soup, this isn't a binary op, curl wants a string here. So if it's
|
|
* TRUE, simply set the value as an empty string as this allows both gzip and
|
|
* zlib compression methods.
|
|
*/
|
|
if (s->accept_compressed_encodings == TRUE) {
|
|
gst_curl_setopt_str (s, handle, CURLOPT_ACCEPT_ENCODING, "");
|
|
} else {
|
|
gst_curl_setopt_str (s, handle, CURLOPT_ACCEPT_ENCODING, "identity");
|
|
}
|
|
|
|
gst_curl_setopt_int (s, handle, CURLOPT_FOLLOWLOCATION,
|
|
s->allow_3xx_redirect);
|
|
gst_curl_setopt_int_default (s, handle, CURLOPT_MAXREDIRS,
|
|
s->max_3xx_redirects);
|
|
gst_curl_setopt_bool (s, handle, CURLOPT_TCP_KEEPALIVE, s->keep_alive);
|
|
gst_curl_setopt_int (s, handle, CURLOPT_TIMEOUT, s->timeout_secs);
|
|
gst_curl_setopt_bool (s, handle, CURLOPT_SSL_VERIFYPEER, s->strict_ssl);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_CAINFO, s->custom_ca_file);
|
|
|
|
switch (s->preferred_http_version) {
|
|
case GSTCURL_HTTP_VERSION_1_0:
|
|
GST_DEBUG_OBJECT (s, "Setting version as HTTP/1.0");
|
|
gst_curl_setopt_int (s, handle, CURLOPT_HTTP_VERSION,
|
|
CURL_HTTP_VERSION_1_0);
|
|
break;
|
|
case GSTCURL_HTTP_VERSION_1_1:
|
|
GST_DEBUG_OBJECT (s, "Setting version as HTTP/1.1");
|
|
gst_curl_setopt_int (s, handle, CURLOPT_HTTP_VERSION,
|
|
CURL_HTTP_VERSION_1_1);
|
|
break;
|
|
#ifdef CURL_VERSION_HTTP2
|
|
case GSTCURL_HTTP_VERSION_2_0:
|
|
GST_DEBUG_OBJECT (s, "Setting version as HTTP/2.0");
|
|
if (curl_easy_setopt (handle, CURLOPT_HTTP_VERSION,
|
|
CURL_HTTP_VERSION_2_0) != CURLE_OK) {
|
|
if (gst_curl_http_src_curl_capabilities->features & CURL_VERSION_HTTP2) {
|
|
GST_WARNING_OBJECT (s,
|
|
"Cannot set unsupported option CURLOPT_HTTP_VERSION");
|
|
} else {
|
|
GST_INFO_OBJECT (s, "HTTP/2 unsupported by libcurl at this time");
|
|
}
|
|
}
|
|
break;
|
|
#endif
|
|
default:
|
|
GST_WARNING_OBJECT (s,
|
|
"Supplied a bogus HTTP version, using curl default!");
|
|
}
|
|
|
|
gst_curl_setopt_generic (s, handle, CURLOPT_HEADERFUNCTION,
|
|
gst_curl_http_src_get_header);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_HEADERDATA, s);
|
|
gst_curl_setopt_generic (s, handle, CURLOPT_WRITEFUNCTION,
|
|
gst_curl_http_src_get_chunks);
|
|
gst_curl_setopt_str (s, handle, CURLOPT_WRITEDATA, s);
|
|
|
|
gst_curl_setopt_str (s, handle, CURLOPT_ERRORBUFFER, s->curl_errbuf);
|
|
|
|
GSTCURL_FUNCTION_EXIT (s);
|
|
return handle;
|
|
}
|
|
|
|
/*
|
|
* Check the return type from the curl transfer. If it was okay, then deal with
|
|
* any headers that were received. Headers should only be dealt with once - but
|
|
* we might get a second set if there are trailing headers (RFC7230 Section 4.4)
|
|
*/
|
|
static GstFlowReturn
|
|
gst_curl_http_src_handle_response (GstCurlHttpSrc * src)
|
|
{
|
|
glong curl_info_long;
|
|
gdouble curl_info_dbl;
|
|
gchar *redirect_url;
|
|
GstBaseSrc *basesrc;
|
|
const GValue *response_headers;
|
|
GstFlowReturn ret = GST_FLOW_OK;
|
|
|
|
GSTCURL_FUNCTION_ENTRY (src);
|
|
|
|
GST_TRACE_OBJECT (src, "status code: %d, curl return code %d",
|
|
src->status_code, src->curl_result);
|
|
|
|
/* Check the curl result code first - anything not 0 is probably a failure */
|
|
if (src->curl_result != 0) {
|
|
GST_WARNING_OBJECT (src, "Curl failed the transfer (%d): %s",
|
|
src->curl_result, curl_easy_strerror (src->curl_result));
|
|
GST_DEBUG_OBJECT (src, "Reason for curl failure: %s", src->curl_errbuf);
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
|
|
/*
|
|
* What response code do we have?
|
|
*/
|
|
if (src->status_code >= 400) {
|
|
GST_WARNING_OBJECT (src, "Transfer for URI %s returned error status %u",
|
|
src->uri, src->status_code);
|
|
src->retries_remaining = 0;
|
|
return GST_FLOW_ERROR;
|
|
} else if (src->status_code == 0) {
|
|
if (curl_easy_getinfo (src->curl_handle, CURLINFO_TOTAL_TIME,
|
|
&curl_info_dbl) != CURLE_OK) {
|
|
/* Curl cannot be relied on in this state, so return an error. */
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
if (curl_info_dbl > src->timeout_secs) {
|
|
return GST_FLOW_CUSTOM_ERROR;
|
|
}
|
|
|
|
if (curl_easy_getinfo (src->curl_handle, CURLINFO_OS_ERRNO,
|
|
&curl_info_long) != CURLE_OK) {
|
|
/* Curl cannot be relied on in this state, so return an error. */
|
|
return GST_FLOW_ERROR;
|
|
|
|
}
|
|
|
|
GST_WARNING_OBJECT (src, "Errno for CONNECT call was %ld (%s)",
|
|
curl_info_long, g_strerror ((gint) curl_info_long));
|
|
|
|
/* Some of these responses are retry-able, others not. Set the returned
|
|
* state to ERROR so we crash out instead of fruitlessly retrying.
|
|
*/
|
|
if (curl_info_long == ECONNREFUSED) {
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
ret = GST_FLOW_CUSTOM_ERROR;
|
|
}
|
|
|
|
|
|
if (ret == GST_FLOW_CUSTOM_ERROR) {
|
|
src->hdrs_updated = FALSE;
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
return ret;
|
|
}
|
|
|
|
/* Only do this once */
|
|
if (src->hdrs_updated == FALSE) {
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
/*
|
|
* Deal with redirections...
|
|
*/
|
|
if (curl_easy_getinfo (src->curl_handle, CURLINFO_EFFECTIVE_URL,
|
|
&redirect_url)
|
|
== CURLE_OK) {
|
|
size_t lena, lenb;
|
|
lena = strlen (src->uri);
|
|
lenb = strlen (redirect_url);
|
|
if (g_ascii_strncasecmp (src->uri, redirect_url,
|
|
(lena > lenb) ? lenb : lena) != 0) {
|
|
GST_INFO_OBJECT (src, "Got a redirect to %s, setting as redirect URI",
|
|
redirect_url);
|
|
src->redirect_uri = g_strdup (redirect_url);
|
|
gst_structure_remove_field (src->http_headers, REDIRECT_URI_NAME);
|
|
gst_structure_set (src->http_headers, REDIRECT_URI_NAME,
|
|
G_TYPE_STRING, redirect_url, NULL);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Push the content length
|
|
*/
|
|
if (curl_easy_getinfo (src->curl_handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD,
|
|
&curl_info_dbl) == CURLE_OK) {
|
|
if (curl_info_dbl == -1) {
|
|
GST_WARNING_OBJECT (src,
|
|
"No Content-Length was specified in the response.");
|
|
} else {
|
|
GST_INFO_OBJECT (src, "Content-Length was given as %.0f", curl_info_dbl);
|
|
basesrc = GST_BASE_SRC_CAST (src);
|
|
basesrc->segment.duration = curl_info_dbl;
|
|
gst_element_post_message (GST_ELEMENT (src),
|
|
gst_message_new_duration_changed (GST_OBJECT (src)));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Push all the received headers down via a sicky event
|
|
*/
|
|
response_headers = gst_structure_get_value (src->http_headers,
|
|
RESPONSE_HEADERS_NAME);
|
|
if (gst_structure_n_fields (gst_value_get_structure (response_headers)) > 0) {
|
|
GstEvent *hdrs_event;
|
|
GstStructure *empty_headers;
|
|
|
|
gst_element_post_message (GST_ELEMENT_CAST (src),
|
|
gst_message_new_element (GST_OBJECT_CAST (src),
|
|
gst_structure_copy (src->http_headers)));
|
|
|
|
/* gst_event_new_custom takes ownership of our structure */
|
|
hdrs_event = gst_event_new_custom (GST_EVENT_CUSTOM_DOWNSTREAM_STICKY,
|
|
src->http_headers);
|
|
gst_pad_push_event (GST_BASE_SRC_PAD (src), hdrs_event);
|
|
GST_INFO_OBJECT (src, "Pushed headers downstream");
|
|
empty_headers = gst_structure_new_empty (RESPONSE_HEADERS_NAME);
|
|
src->http_headers = gst_structure_new (HTTP_HEADERS_NAME,
|
|
URI_NAME, G_TYPE_STRING, src->uri,
|
|
REQUEST_HEADERS_NAME, GST_TYPE_STRUCTURE, src->request_headers,
|
|
RESPONSE_HEADERS_NAME, GST_TYPE_STRUCTURE,
|
|
gst_structure_new_empty (RESPONSE_HEADERS_NAME), NULL);
|
|
gst_structure_free (empty_headers);
|
|
}
|
|
|
|
src->hdrs_updated = FALSE;
|
|
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* "Negotiate" capabilities between us and the sink.
|
|
* I.e. tell the sink device what data to expect. We can't be told what to send
|
|
* unless we implement "only return to me if this type" property. Potential TODO
|
|
*/
|
|
static gboolean
|
|
gst_curl_http_src_negotiate_caps (GstCurlHttpSrc * src)
|
|
{
|
|
GST_INFO_OBJECT (src, "Negotiating caps...");
|
|
if (src->caps && src->http_headers) {
|
|
const GValue *response_headers = gst_structure_get_value (src->http_headers,
|
|
RESPONSE_HEADERS_NAME);
|
|
|
|
if (gst_structure_has_field (gst_value_get_structure (response_headers),
|
|
"content-type") == TRUE) {
|
|
const GValue *gv_content_type =
|
|
gst_structure_get_value (gst_value_get_structure (response_headers),
|
|
"content-type");
|
|
if (G_VALUE_HOLDS_STRING (gv_content_type) == TRUE) {
|
|
const gchar *content_type = g_value_get_string (gv_content_type);
|
|
GST_INFO_OBJECT (src, "Setting caps as Content-Type of %s",
|
|
content_type);
|
|
src->caps = gst_caps_make_writable (src->caps);
|
|
gst_caps_set_simple (src->caps, "content-type", G_TYPE_STRING,
|
|
content_type, NULL);
|
|
if (gst_base_src_set_caps (GST_BASE_SRC (src), src->caps) != TRUE) {
|
|
GST_ERROR_OBJECT (src, "Setting caps failed!");
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
GST_ERROR_OBJECT (src, "Content Type doesn't contain expected string");
|
|
return FALSE;
|
|
}
|
|
}
|
|
} else {
|
|
GST_DEBUG_OBJECT (src, "No caps have been set, continue.");
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* Cleanup the CURL easy handle once we're done with it.
|
|
*/
|
|
static inline void
|
|
gst_curl_http_src_destroy_easy_handle (GstCurlHttpSrc * src)
|
|
{
|
|
/* Thank you Handles, and well done. Well done, mate. */
|
|
if (src->curl_handle != NULL) {
|
|
curl_easy_cleanup (src->curl_handle);
|
|
src->curl_handle = NULL;
|
|
}
|
|
/* In addition, clean up the curl header slist if it was used. */
|
|
if (src->slist != NULL) {
|
|
curl_slist_free_all (src->slist);
|
|
src->slist = NULL;
|
|
}
|
|
}
|
|
|
|
static GstStateChangeReturn
|
|
gst_curl_http_src_change_state (GstElement * element, GstStateChange transition)
|
|
{
|
|
GstStateChangeReturn ret;
|
|
GstCurlHttpSrc *source = GST_CURLHTTPSRC (element);
|
|
GSTCURL_FUNCTION_ENTRY (source);
|
|
|
|
switch (transition) {
|
|
case GST_STATE_CHANGE_NULL_TO_READY:
|
|
gst_curl_http_src_ref_multi (source);
|
|
break;
|
|
case GST_STATE_CHANGE_READY_TO_NULL:
|
|
/* The pipeline has ended, so signal any running request to end. */
|
|
gst_curl_http_src_request_remove (source);
|
|
gst_curl_http_src_unref_multi (source);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
|
|
|
GSTCURL_FUNCTION_EXIT (source);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Take care of any memory that may be left over from the instance that's now
|
|
* closing before we leak it.
|
|
*/
|
|
static void
|
|
gst_curl_http_src_cleanup_instance (GstCurlHttpSrc * src)
|
|
{
|
|
gint i;
|
|
g_mutex_lock (&src->uri_mutex);
|
|
g_free (src->uri);
|
|
src->uri = NULL;
|
|
g_free (src->redirect_uri);
|
|
src->redirect_uri = NULL;
|
|
g_mutex_unlock (&src->uri_mutex);
|
|
g_mutex_clear (&src->uri_mutex);
|
|
|
|
g_free (src->proxy_uri);
|
|
src->proxy_uri = NULL;
|
|
g_free (src->no_proxy_list);
|
|
src->no_proxy_list = NULL;
|
|
g_free (src->proxy_user);
|
|
src->proxy_user = NULL;
|
|
g_free (src->proxy_pass);
|
|
src->proxy_pass = NULL;
|
|
|
|
for (i = 0; i < src->number_cookies; i++) {
|
|
g_free (src->cookies[i]);
|
|
src->cookies[i] = NULL;
|
|
}
|
|
g_free (src->cookies);
|
|
src->cookies = NULL;
|
|
|
|
g_mutex_clear (&src->buffer_mutex);
|
|
|
|
g_cond_clear (&src->signal);
|
|
|
|
g_free (src->buffer);
|
|
src->buffer = NULL;
|
|
|
|
if (src->http_headers != NULL) {
|
|
gst_structure_free (src->http_headers);
|
|
src->http_headers = NULL;
|
|
}
|
|
|
|
gst_curl_http_src_destroy_easy_handle (src);
|
|
}
|
|
|
|
static gboolean
|
|
gst_curl_http_src_query (GstBaseSrc * bsrc, GstQuery * query)
|
|
{
|
|
GstCurlHttpSrc *src = GST_CURLHTTPSRC (bsrc);
|
|
gboolean ret;
|
|
GSTCURL_FUNCTION_ENTRY (src);
|
|
|
|
switch (GST_QUERY_TYPE (query)) {
|
|
case GST_QUERY_URI:
|
|
gst_query_set_uri (query, src->uri);
|
|
if (src->redirect_uri != NULL) {
|
|
gst_query_set_uri_redirection (query, src->redirect_uri);
|
|
}
|
|
ret = TRUE;
|
|
break;
|
|
default:
|
|
ret = GST_BASE_SRC_CLASS (parent_class)->query (bsrc, query);
|
|
break;
|
|
}
|
|
|
|
GSTCURL_FUNCTION_EXIT (src);
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
gst_curl_http_src_get_content_length (GstBaseSrc * bsrc, guint64 * size)
|
|
{
|
|
GstCurlHttpSrc *src = GST_CURLHTTPSRC (bsrc);
|
|
const GValue *response_headers;
|
|
gboolean ret = FALSE;
|
|
|
|
if (src->http_headers == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
response_headers = gst_structure_get_value (src->http_headers,
|
|
RESPONSE_HEADERS_NAME);
|
|
if (gst_structure_has_field (gst_value_get_structure (response_headers),
|
|
"content-length") == TRUE) {
|
|
const GValue *content_length =
|
|
gst_structure_get_value (gst_value_get_structure (response_headers),
|
|
"content-length");
|
|
if (G_VALUE_HOLDS_STRING (content_length) == TRUE) {
|
|
const gchar *len = g_value_get_string (content_length);
|
|
*size = (guint64) g_ascii_strtoull (len, NULL, 10);
|
|
ret = TRUE;
|
|
} else {
|
|
GST_ERROR_OBJECT (src, "Content Length doesn't contain expected string");
|
|
}
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (src,
|
|
"No content length has yet been set, or there was an error!");
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
gst_curl_http_src_uri_handler_init (gpointer g_iface, gpointer iface_data)
|
|
{
|
|
GstURIHandlerInterface *uri_iface = (GstURIHandlerInterface *) g_iface;
|
|
|
|
uri_iface->get_type = gst_curl_http_src_urihandler_get_type;
|
|
uri_iface->get_protocols = gst_curl_http_src_urihandler_get_protocols;
|
|
uri_iface->get_uri = gst_curl_http_src_urihandler_get_uri;
|
|
uri_iface->set_uri = gst_curl_http_src_urihandler_set_uri;
|
|
}
|
|
|
|
static guint
|
|
gst_curl_http_src_urihandler_get_type (GType type)
|
|
{
|
|
return GST_URI_SRC;
|
|
}
|
|
|
|
static const gchar *const *
|
|
gst_curl_http_src_urihandler_get_protocols (GType type)
|
|
{
|
|
static const gchar *protocols[] = { "http", "https", NULL };
|
|
|
|
return protocols;
|
|
}
|
|
|
|
static gchar *
|
|
gst_curl_http_src_urihandler_get_uri (GstURIHandler * handler)
|
|
{
|
|
gchar *ret;
|
|
GstCurlHttpSrc *source;
|
|
|
|
g_return_val_if_fail (GST_IS_URI_HANDLER (handler), FALSE);
|
|
source = GST_CURLHTTPSRC (handler);
|
|
|
|
GSTCURL_FUNCTION_ENTRY (source);
|
|
|
|
g_mutex_lock (&source->uri_mutex);
|
|
ret = g_strdup (source->uri);
|
|
g_mutex_unlock (&source->uri_mutex);
|
|
|
|
GSTCURL_FUNCTION_EXIT (source);
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
gst_curl_http_src_urihandler_set_uri (GstURIHandler * handler,
|
|
const gchar * uri, GError ** error)
|
|
{
|
|
GstCurlHttpSrc *source = GST_CURLHTTPSRC (handler);
|
|
GSTCURL_FUNCTION_ENTRY (source);
|
|
|
|
g_return_val_if_fail (GST_IS_URI_HANDLER (handler), FALSE);
|
|
g_return_val_if_fail (uri != NULL, FALSE);
|
|
|
|
g_mutex_lock (&source->uri_mutex);
|
|
|
|
if (source->uri != NULL) {
|
|
GST_DEBUG_OBJECT (source,
|
|
"URI already present as %s, updating to new URI %s", source->uri, uri);
|
|
g_free (source->uri);
|
|
}
|
|
|
|
source->uri = g_strdup (uri);
|
|
if (source->uri == NULL) {
|
|
return FALSE;
|
|
}
|
|
source->retries_remaining = source->total_retries;
|
|
|
|
g_mutex_unlock (&source->uri_mutex);
|
|
|
|
GSTCURL_FUNCTION_EXIT (source);
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* Cancel any currently running transfer, and then signal all the loops to drop
|
|
* any received buffers. The ::create() method should return GST_FLOW_FLUSHING.
|
|
*/
|
|
static gboolean
|
|
gst_curl_http_src_unlock (GstBaseSrc * bsrc)
|
|
{
|
|
GstCurlHttpSrc *src = GST_CURLHTTPSRC (bsrc);
|
|
|
|
g_mutex_lock (&src->buffer_mutex);
|
|
if (src->state != GSTCURL_UNLOCK) {
|
|
if (src->state == GSTCURL_OK) {
|
|
/* A transfer is running, cancel it */
|
|
gst_curl_http_src_request_remove (src);
|
|
}
|
|
src->pending_state = src->state;
|
|
src->state = GSTCURL_UNLOCK;
|
|
}
|
|
g_cond_signal (&src->signal);
|
|
g_mutex_unlock (&src->buffer_mutex);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* Finish the unlock request above and return curlhttpsrc to the normal state.
|
|
* This will probably be GSTCURL_DONE, and the next return from ::create() will
|
|
* be GST_FLOW_EOS as we don't want to deliver parts of a HTTP body.
|
|
*/
|
|
static gboolean
|
|
gst_curl_http_src_unlock_stop (GstBaseSrc * bsrc)
|
|
{
|
|
GstCurlHttpSrc *src = GST_CURLHTTPSRC (bsrc);
|
|
|
|
g_mutex_lock (&src->buffer_mutex);
|
|
src->state = src->pending_state;
|
|
src->pending_state = GSTCURL_NONE;
|
|
g_cond_signal (&src->signal);
|
|
g_mutex_unlock (&src->buffer_mutex);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
* Curl loop task functions begin
|
|
*****************************************************************************/
|
|
static void
|
|
gst_curl_http_src_curl_multi_loop (gpointer thread_data)
|
|
{
|
|
GstCurlHttpSrcMultiTaskContext *context;
|
|
GstCurlHttpSrcQueueElement *qelement;
|
|
int i, still_running;
|
|
gboolean cond = FALSE;
|
|
CURLMsg *curl_message;
|
|
|
|
context = (GstCurlHttpSrcMultiTaskContext *) thread_data;
|
|
|
|
g_mutex_lock (&context->mutex);
|
|
|
|
/* Someone is holding a reference to us, but isn't using us so to avoid
|
|
* unnecessary clock cycle wasting, sit in a conditional wait until woken.
|
|
*/
|
|
while (context->state == GSTCURL_MULTI_LOOP_STATE_WAIT) {
|
|
GSTCURL_DEBUG_PRINT ("Entering wait state...");
|
|
g_cond_wait (&context->signal, &context->mutex);
|
|
GSTCURL_DEBUG_PRINT ("Received wake up call!");
|
|
}
|
|
|
|
if (context->state == GSTCURL_MULTI_LOOP_STATE_QUEUE_EVENT) {
|
|
GSTCURL_DEBUG_PRINT ("Received a new item on the queue!");
|
|
if (context->queue == NULL) {
|
|
GSTCURL_ERROR_PRINT ("Request Queue was empty on a Queue Event!");
|
|
context->state = GSTCURL_MULTI_LOOP_STATE_WAIT;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Use the running mutex to lock access to each element, as the
|
|
* mutex's memory barriers stop cache optimisations from meaning
|
|
* flag values can't be trusted. The trylock will only let us in
|
|
* once and should fail immediately prior.
|
|
*/
|
|
qelement = context->queue;
|
|
while (qelement != NULL) {
|
|
if (g_mutex_trylock (&qelement->running) == TRUE) {
|
|
GSTCURL_DEBUG_PRINT ("Adding easy handle for URI %s", qelement->p->uri);
|
|
cond = TRUE;
|
|
curl_multi_add_handle (context->multi_handle, qelement->p->curl_handle);
|
|
}
|
|
qelement = qelement->next;
|
|
}
|
|
|
|
if (cond != TRUE) {
|
|
GSTCURL_WARNING_PRINT ("All curl handles already added for QUEUE_EVENT!");
|
|
} else {
|
|
GSTCURL_DEBUG_PRINT ("Finished adding all handles, continuing.");
|
|
context->state = GSTCURL_MULTI_LOOP_STATE_RUNNING;
|
|
}
|
|
g_mutex_unlock (&context->mutex);
|
|
} else if (context->state == GSTCURL_MULTI_LOOP_STATE_RUNNING) {
|
|
struct timeval timeout;
|
|
gint rc;
|
|
fd_set fdread, fdwrite, fdexcep;
|
|
int maxfd = -1;
|
|
long curl_timeo = -1;
|
|
|
|
/* Because curl can possibly take some time here, be nice and let go of the
|
|
* mutex so other threads can perform state/queue operations as we don't
|
|
* care about those until the end of this. */
|
|
g_mutex_unlock (&context->mutex);
|
|
|
|
FD_ZERO (&fdread);
|
|
FD_ZERO (&fdwrite);
|
|
FD_ZERO (&fdexcep);
|
|
|
|
timeout.tv_sec = 1;
|
|
timeout.tv_usec = 0;
|
|
|
|
curl_multi_timeout (context->multi_handle, &curl_timeo);
|
|
if (curl_timeo >= 0) {
|
|
timeout.tv_sec = curl_timeo / 1000;
|
|
if (timeout.tv_sec > 1) {
|
|
timeout.tv_sec = 1;
|
|
} else {
|
|
timeout.tv_usec = (curl_timeo % 1000) * 1000;
|
|
}
|
|
}
|
|
|
|
/* get file descriptors from the transfers */
|
|
curl_multi_fdset (context->multi_handle, &fdread, &fdwrite, &fdexcep,
|
|
&maxfd);
|
|
|
|
rc = select (maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
|
|
|
|
switch (rc) {
|
|
case -1:
|
|
/* select error */
|
|
break;
|
|
case 0:
|
|
default:
|
|
/* timeout or readable/writable sockets */
|
|
curl_multi_perform (context->multi_handle, &still_running);
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Check the CURL message buffer to find out if any transfers have
|
|
* completed. If they have, call the signal_finished function which
|
|
* will signal the g_cond_wait call in that calling instance.
|
|
*/
|
|
i = 0;
|
|
while (cond != TRUE) {
|
|
curl_message = curl_multi_info_read (context->multi_handle, &i);
|
|
if (curl_message == NULL) {
|
|
cond = TRUE;
|
|
} else if (curl_message->msg == CURLMSG_DONE) {
|
|
/* A hack, but I have seen curl_message->easy_handle being
|
|
* NULL randomly, so check for that. */
|
|
g_mutex_lock (&context->mutex);
|
|
if (curl_message->easy_handle == NULL) {
|
|
break;
|
|
}
|
|
curl_multi_remove_handle (context->multi_handle,
|
|
curl_message->easy_handle);
|
|
gst_curl_http_src_remove_queue_handle (&context->queue,
|
|
curl_message->easy_handle, curl_message->data.result);
|
|
g_mutex_unlock (&context->mutex);
|
|
}
|
|
}
|
|
|
|
if (still_running == 0) {
|
|
/* We've finished processing, so set the state to wait.
|
|
*
|
|
* This is a little more complex, as we need to catch the edge
|
|
* case of another thread adding a queue item while we've been
|
|
* working.
|
|
*/
|
|
g_mutex_lock (&context->mutex);
|
|
if ((context->state != GSTCURL_MULTI_LOOP_STATE_QUEUE_EVENT) &&
|
|
(context->state != GSTCURL_MULTI_LOOP_STATE_REQUEST_REMOVAL)) {
|
|
context->state = GSTCURL_MULTI_LOOP_STATE_WAIT;
|
|
}
|
|
g_mutex_unlock (&context->mutex);
|
|
}
|
|
}
|
|
/* Is the following even necessary any more...? */
|
|
else if (context->state == GSTCURL_MULTI_LOOP_STATE_STOP) {
|
|
g_mutex_unlock (&context->mutex);
|
|
/* Something wants us to shut down, so best to do a full cleanup as it
|
|
* might be that something's gone bang.
|
|
*/
|
|
/*gst_curl_http_src_unref_multi (NULL, GSTCURL_RETURN_PIPELINE_NULL, TRUE); */
|
|
GSTCURL_INFO_PRINT ("Got instruction to shut down");
|
|
} else if (context->state == GSTCURL_MULTI_LOOP_STATE_REQUEST_REMOVAL) {
|
|
qelement = context->queue;
|
|
while (qelement != NULL) {
|
|
if (qelement->p == context->request_removal_element) {
|
|
g_mutex_lock (&qelement->p->buffer_mutex);
|
|
curl_multi_remove_handle (context->multi_handle,
|
|
context->request_removal_element->curl_handle);
|
|
if (qelement->p->state == GSTCURL_UNLOCK) {
|
|
qelement->p->pending_state = GSTCURL_REMOVED;
|
|
} else {
|
|
qelement->p->state = GSTCURL_REMOVED;
|
|
}
|
|
g_cond_signal (&qelement->p->signal);
|
|
g_mutex_unlock (&qelement->p->buffer_mutex);
|
|
gst_curl_http_src_remove_queue_item (&context->queue, qelement->p);
|
|
}
|
|
}
|
|
context->request_removal_element = NULL;
|
|
g_mutex_unlock (&context->mutex);
|
|
} else {
|
|
GSTCURL_WARNING_PRINT ("Curl Loop State was invalid or unsupported");
|
|
GSTCURL_WARNING_PRINT ("Signal State is %d, resetting to RUNNING.",
|
|
context->state);
|
|
/* Reset to running, so if there isn't anything to do it'll be
|
|
* changed the WAIT once curl_multi_perform says it has no active
|
|
* handles. */
|
|
context->state = GSTCURL_MULTI_LOOP_STATE_RUNNING;
|
|
g_mutex_unlock (&context->mutex);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Receive headers from the remote server and put them into the http_headers
|
|
* structure to be sent downstream when we've got them all and started receiving
|
|
* the body (see ::_handle_response())
|
|
*/
|
|
static size_t
|
|
gst_curl_http_src_get_header (void *header, size_t size, size_t nmemb,
|
|
void *src)
|
|
{
|
|
GstCurlHttpSrc *s = src;
|
|
char *substr;
|
|
|
|
GST_DEBUG_OBJECT (s, "Received header: %s", (char *) header);
|
|
|
|
g_mutex_lock (&s->buffer_mutex);
|
|
|
|
if (s->state == GSTCURL_UNLOCK) {
|
|
g_mutex_unlock (&s->buffer_mutex);
|
|
return size * nmemb;
|
|
}
|
|
|
|
if (s->http_headers == NULL) {
|
|
/* Can't do anything here, so just silently swallow the header */
|
|
GST_DEBUG_OBJECT (s, "HTTP Headers Structure has already been sent,"
|
|
" ignoring header");
|
|
g_mutex_unlock (&s->buffer_mutex);
|
|
return size * nmemb;
|
|
}
|
|
|
|
substr = gst_curl_http_src_strcasestr (header, "HTTP");
|
|
if (substr == header) {
|
|
/* We have a status line! */
|
|
gchar **status_line_fields;
|
|
|
|
/* Have we already seen a status line? If so, delete any response headers */
|
|
if (s->status_code > 0) {
|
|
GstStructure *empty_headers =
|
|
gst_structure_new_empty (RESPONSE_HEADERS_NAME);
|
|
gst_structure_remove_field (s->http_headers, RESPONSE_HEADERS_NAME);
|
|
gst_structure_set (s->http_headers, RESPONSE_HEADERS_NAME,
|
|
GST_TYPE_STRUCTURE, empty_headers, NULL);
|
|
gst_structure_free (empty_headers);
|
|
|
|
}
|
|
|
|
/* Process the status line */
|
|
status_line_fields = g_strsplit ((gchar *) header, " ", 3);
|
|
if (status_line_fields == NULL) {
|
|
GST_ERROR_OBJECT (s, "Status line processing failed!");
|
|
} else {
|
|
s->status_code =
|
|
(guint) g_ascii_strtoll (status_line_fields[1], NULL, 10);
|
|
GST_INFO_OBJECT (s, "Received status %u for request for URI %s: %s",
|
|
s->status_code, s->uri, status_line_fields[2]);
|
|
gst_structure_set (s->http_headers, HTTP_STATUS_CODE,
|
|
G_TYPE_UINT, s->status_code, NULL);
|
|
g_strfreev (status_line_fields);
|
|
}
|
|
} else {
|
|
/* Normal header line */
|
|
gchar **header_tpl = g_strsplit ((gchar *) header, ": ", 2);
|
|
if (header_tpl == NULL) {
|
|
GST_ERROR_OBJECT (s, "Header processing failed! (%s)", (gchar *) header);
|
|
} else {
|
|
const GValue *gv_resp_hdrs = gst_structure_get_value (s->http_headers,
|
|
RESPONSE_HEADERS_NAME);
|
|
const GstStructure *response_headers =
|
|
gst_value_get_structure (gv_resp_hdrs);
|
|
/* Store header key lower case (g_ascii_strdown), makes searching through
|
|
* later on easier - end applications shouldn't care, as all HTTP headers
|
|
* are case-insensitive */
|
|
gchar *header_key = g_ascii_strdown (header_tpl[0], -1);
|
|
gchar *header_value;
|
|
|
|
/* If header field already exists, append to the end */
|
|
if (gst_structure_has_field (response_headers, header_key) == TRUE) {
|
|
header_value = g_strdup_printf ("%s, %s",
|
|
g_value_get_string (gst_structure_get_value (response_headers,
|
|
header_key)), header_tpl[1]);
|
|
gst_structure_set ((GstStructure *) response_headers, header_key,
|
|
G_TYPE_STRING, header_value, NULL);
|
|
g_free (header_value);
|
|
} else {
|
|
header_value = header_tpl[1];
|
|
gst_structure_set ((GstStructure *) response_headers, header_key,
|
|
G_TYPE_STRING, header_value, NULL);
|
|
}
|
|
|
|
/* We have some special cases - deal with them here */
|
|
if (g_strcmp0 (header_key, "content-type") == 0) {
|
|
gst_curl_http_src_negotiate_caps (src);
|
|
}
|
|
|
|
g_free (header_key);
|
|
g_strfreev (header_tpl);
|
|
}
|
|
}
|
|
|
|
s->hdrs_updated = TRUE;
|
|
|
|
g_mutex_unlock (&s->buffer_mutex);
|
|
|
|
return size * nmemb;
|
|
}
|
|
|
|
/*
|
|
* My own quick and dirty implementation of strcasestr. This is a GNU extension
|
|
* (i.e. not portable) and not always guaranteed to be available.
|
|
*
|
|
* I know this doesn't work if the haystack and needle are the same size. But
|
|
* this isn't necessarily a bad thing, as the only place we currently use this
|
|
* is at a point where returning nothing even if a string match occurs but the
|
|
* needle is the same size as the haystack actually saves us time.
|
|
*/
|
|
static char *
|
|
gst_curl_http_src_strcasestr (const char *haystack, const char *needle)
|
|
{
|
|
int i, j, needle_len;
|
|
char *location;
|
|
|
|
needle_len = (int) strlen (needle);
|
|
i = 0;
|
|
j = 0;
|
|
location = NULL;
|
|
|
|
while (haystack[i] != '\0') {
|
|
if (j == needle_len) {
|
|
location = (char *) haystack + (i - j);
|
|
}
|
|
if (tolower (haystack[i]) == tolower (needle[j])) {
|
|
j++;
|
|
} else {
|
|
j = 0;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
return location;
|
|
}
|
|
|
|
/*
|
|
* Receive chunks of the requested body and pass these back to the ::create()
|
|
* loop
|
|
*/
|
|
static size_t
|
|
gst_curl_http_src_get_chunks (void *chunk, size_t size, size_t nmemb, void *src)
|
|
{
|
|
GstCurlHttpSrc *s = src;
|
|
size_t chunk_len = size * nmemb;
|
|
GST_TRACE_OBJECT (s,
|
|
"Received curl chunk for URI %s of size %d", s->uri, (int) chunk_len);
|
|
g_mutex_lock (&s->buffer_mutex);
|
|
if (s->state == GSTCURL_UNLOCK) {
|
|
g_mutex_unlock (&s->buffer_mutex);
|
|
return chunk_len;
|
|
}
|
|
s->buffer =
|
|
g_realloc (s->buffer, (s->buffer_len + chunk_len + 1) * sizeof (char));
|
|
if (s->buffer == NULL) {
|
|
GST_ERROR_OBJECT (s, "Realloc for cURL response message failed!\n");
|
|
return 0;
|
|
}
|
|
memcpy (s->buffer + s->buffer_len, chunk, chunk_len);
|
|
s->buffer_len += chunk_len;
|
|
g_cond_signal (&s->signal);
|
|
g_mutex_unlock (&s->buffer_mutex);
|
|
return chunk_len;
|
|
}
|
|
|
|
/*
|
|
* Request a cancellation of a currently running curl handle.
|
|
*/
|
|
static void
|
|
gst_curl_http_src_request_remove (GstCurlHttpSrc * src)
|
|
{
|
|
GstCurlHttpSrcClass *klass = G_TYPE_INSTANCE_GET_CLASS (src,
|
|
GST_TYPE_CURL_HTTP_SRC,
|
|
GstCurlHttpSrcClass);
|
|
g_mutex_lock (&klass->multi_task_context.mutex);
|
|
|
|
klass->multi_task_context.state = GSTCURL_MULTI_LOOP_STATE_REQUEST_REMOVAL;
|
|
klass->multi_task_context.request_removal_element = src;
|
|
g_cond_signal (&klass->multi_task_context.signal);
|
|
g_mutex_unlock (&klass->multi_task_context.mutex);
|
|
}
|