--- /dev/null
+/* GStreamer
+ * Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
+ * Copyright (C) 2010 Andoni Morales Alastruey <ylatuya@gmail.com>
+ *
+ * Gsthlsdemux.c:
+ *
+ * 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-hlsdemux
+ *
+ * HTTP Live Streaming source element.
+ *
+ * <refsect2>
+ * <title>Example launch line</title>
+ * |[
+ * gst-launch hlsdemux location=http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8 ! decodebin2 ! xvimagesink
+ * ]|
+ * </refsect2>
+ *
+ * Last reviewed on 2010-10-07
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+
+#include <string.h>
+#include "gsthlsdemux.h"
+
+static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
+ GST_PAD_SRC,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS ("video/mpegts, systemstream=(boolean)true; "
+ "video/webm"));
+
+static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
+ GST_PAD_SINK,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS ("playlist/m3u8"));
+
+static GstStaticPadTemplate fetchertemplate = GST_STATIC_PAD_TEMPLATE ("sink",
+ GST_PAD_SINK,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS_ANY);
+
+GST_DEBUG_CATEGORY_STATIC (gst_hls_demux_debug);
+#define GST_CAT_DEFAULT gst_hls_demux_debug
+
+enum
+{
+ PROP_0,
+
+ PROP_FRAGMENTS_CACHE,
+ PROP_BITRATE_SWITCH_TOLERANCE,
+ PROP_LAST
+};
+
+static const float update_interval_factor[] = { 1, 0.5, 1.5, 3 };
+
+#define DEFAULT_FRAGMENTS_CACHE 3
+#define DEFAULT_FAILED_COUNT 3
+#define DEFAULT_BITRATE_SWITCH_TOLERANCE 0.4
+
+/* GObject */
+static void gst_hls_demux_set_property (GObject * object, guint prop_id,
+ const GValue * value, GParamSpec * pspec);
+static void gst_hls_demux_get_property (GObject * object, guint prop_id,
+ GValue * value, GParamSpec * pspec);
+static void gst_hls_demux_dispose (GObject * obj);
+
+/* GstElement */
+static GstStateChangeReturn
+gst_hls_demux_change_state (GstElement * element, GstStateChange transition);
+
+/* GstHLSDemux */
+static GstBusSyncReply gst_hls_demux_fetcher_bus_handler (GstBus * bus,
+ GstMessage * message, gpointer data);
+static GstFlowReturn gst_hls_demux_chain (GstPad * pad, GstBuffer * buf);
+static gboolean gst_hls_demux_sink_event (GstPad * pad, GstEvent * event);
+static GstFlowReturn gst_hls_demux_fetcher_chain (GstPad * pad, GstBuffer * buf);
+static gboolean gst_hls_demux_fetcher_sink_event (GstPad * pad, GstEvent * event);
+static void gst_hls_demux_loop (GstHLSDemux * demux);
+static void gst_hls_demux_stop (GstHLSDemux * demux);
+static void gst_hls_demux_stop_fetcher (GstHLSDemux * demux, gboolean cancelled);
+static gboolean gst_hls_demux_start_update (GstHLSDemux * demux);
+static gboolean gst_hls_demux_cache_fragments (GstHLSDemux * demux);
+static gboolean gst_hls_demux_schedule (GstHLSDemux * demux);
+static gboolean gst_hls_demux_switch_playlist (GstHLSDemux * demux);
+static gboolean gst_hls_demux_get_next_fragment (GstHLSDemux * demux, gboolean retry);
+static gboolean gst_hls_demux_update_playlist (GstHLSDemux * demux, gboolean retry);
+static void gst_hls_demux_reset (GstHLSDemux * demux);
+static gboolean gst_hls_demux_set_location (GstHLSDemux * demux, const gchar * uri);
+
+static void
+_do_init (GType type)
+{
+ GST_DEBUG_CATEGORY_INIT (gst_hls_demux_debug, "hlsdemux", 0, "hlsdemux element");
+}
+
+GST_BOILERPLATE_FULL (GstHLSDemux, gst_hls_demux, GstElement,
+ GST_TYPE_ELEMENT, _do_init);
+
+static void
+gst_hls_demux_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_add_pad_template (element_class,
+ gst_static_pad_template_get (&sinktemplate));
+
+ gst_element_class_set_details_simple (element_class,
+ "HLS Source",
+ "Decoder",
+ "HTTP Live Streaming source",
+ "Marc-Andre Lureau <marcandre.lureau@gmail.com>\n"
+ "Andoni Morales Alastruey <ylatuya@gmail.com>");
+}
+
+static void
+gst_hls_demux_dispose (GObject * obj)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (obj);
+
+ gst_hls_demux_stop_fetcher (demux, TRUE);
+ g_cond_free (demux->fetcher_cond);
+ g_mutex_free (demux->fetcher_lock);
+
+ if (demux->client) {
+ gst_m3u8_client_free (demux->client);
+ demux->client = NULL;
+ }
+
+ g_cond_free (demux->thread_cond);
+ g_mutex_free (demux->thread_lock);
+
+ if (GST_TASK_STATE (demux->task) != GST_TASK_STOPPED) {
+ gst_task_stop (demux->task);
+ gst_task_join (demux->task);
+ }
+ gst_object_unref (demux->task);
+ g_static_rec_mutex_free (&demux->task_lock);
+
+ while (!g_queue_is_empty (demux->queue)) {
+ GstBuffer *buf = g_queue_pop_head (demux->queue);
+ gst_buffer_unref (buf);
+ }
+ g_queue_free (demux->queue);
+
+ gst_object_unref (demux->fetcher_bus);
+ gst_object_unref (demux->fetcherpad);
+
+ G_OBJECT_CLASS (parent_class)->dispose (obj);
+}
+
+static void
+gst_hls_demux_class_init (GstHLSDemuxClass * klass)
+{
+ GObjectClass *gobject_class;
+ GstElementClass *gstelement_class;
+
+ gobject_class = G_OBJECT_CLASS (klass);
+ gstelement_class = (GstElementClass *) klass;
+
+ parent_class = g_type_class_peek_parent (klass);
+
+ gobject_class->set_property = gst_hls_demux_set_property;
+ gobject_class->get_property = gst_hls_demux_get_property;
+ gobject_class->dispose = gst_hls_demux_dispose;
+
+ g_object_class_install_property (gobject_class, PROP_FRAGMENTS_CACHE,
+ g_param_spec_uint ("fragments-cache", "Fragments cache",
+ "Number of fragments needed to be cached to start playing",
+ 2, G_MAXUINT, DEFAULT_FRAGMENTS_CACHE, G_PARAM_READWRITE));
+
+ g_object_class_install_property (gobject_class, PROP_BITRATE_SWITCH_TOLERANCE,
+ g_param_spec_float ("bitrate-switch-tolerance",
+ "Bitrate switch tolerance",
+ "Tolerance with respect of the fragment duration to switch to "
+ "a different bitrate if the client is too slow/fast.",
+ 0, 1, DEFAULT_BITRATE_SWITCH_TOLERANCE, G_PARAM_READWRITE));
+
+ gstelement_class->change_state = gst_hls_demux_change_state;
+}
+
+static void
+gst_hls_demux_init (GstHLSDemux * demux, GstHLSDemuxClass * klass)
+{
+ /* sink pad */
+ demux->sinkpad = gst_pad_new_from_static_template (&sinktemplate, "sink");
+ gst_pad_set_chain_function (demux->sinkpad,
+ GST_DEBUG_FUNCPTR (gst_hls_demux_chain));
+ gst_pad_set_event_function (demux->sinkpad,
+ GST_DEBUG_FUNCPTR (gst_hls_demux_sink_event));
+ gst_element_add_pad (GST_ELEMENT (demux), demux->sinkpad);
+
+ /* demux pad */
+ demux->srcpad = gst_pad_new_from_static_template (&srctemplate, "src");
+ gst_element_add_pad (GST_ELEMENT (demux), demux->srcpad);
+
+ /* fetcher pad */
+ demux->fetcherpad = gst_pad_new_from_static_template (&fetchertemplate, "sink");
+ gst_pad_set_chain_function (demux->fetcherpad,
+ GST_DEBUG_FUNCPTR (gst_hls_demux_fetcher_chain));
+ gst_pad_set_event_function (demux->fetcherpad,
+ GST_DEBUG_FUNCPTR (gst_hls_demux_fetcher_sink_event));
+ gst_pad_set_element_private (demux->fetcherpad, demux);
+ gst_pad_activate_push (demux->fetcherpad, TRUE);
+
+ /* Properties */
+ demux->fragments_cache = DEFAULT_FRAGMENTS_CACHE;
+ demux->bitrate_switch_tol = DEFAULT_BITRATE_SWITCH_TOLERANCE;
+
+ demux->fetcher_bus = gst_bus_new ();
+ gst_bus_set_sync_handler (demux->fetcher_bus, gst_hls_demux_fetcher_bus_handler,
+ demux);
+ demux->thread_cond = g_cond_new ();
+ demux->thread_lock = g_mutex_new ();
+ demux->fetcher_cond = g_cond_new ();
+ demux->fetcher_lock = g_mutex_new ();
+ demux->queue = g_queue_new ();
+ g_static_rec_mutex_init (&demux->task_lock);
+ demux->task = gst_task_create ((GstTaskFunction) gst_hls_demux_loop, demux);
+ gst_task_set_lock (demux->task, &demux->task_lock);
+
+ gst_hls_demux_reset (demux);
+}
+
+static void
+gst_hls_demux_set_property (GObject * object, guint prop_id, const GValue * value,
+ GParamSpec * pspec)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (object);
+
+ switch (prop_id) {
+ case PROP_FRAGMENTS_CACHE:
+ demux->fragments_cache = g_value_get_uint (value);
+ break;
+ case PROP_BITRATE_SWITCH_TOLERANCE:
+ demux->bitrate_switch_tol = g_value_get_float (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_hls_demux_get_property (GObject * object, guint prop_id, GValue * value,
+ GParamSpec * pspec)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (object);
+
+ switch (prop_id) {
+ case PROP_FRAGMENTS_CACHE:
+ g_value_set_uint (value, demux->fragments_cache);
+ break;
+ case PROP_BITRATE_SWITCH_TOLERANCE:
+ g_value_set_float (value, demux->bitrate_switch_tol);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static GstStateChangeReturn
+gst_hls_demux_change_state (GstElement * element, GstStateChange transition)
+{
+ GstStateChangeReturn ret;
+ GstHLSDemux *demux = GST_HLS_DEMUX (element);
+
+ switch (transition) {
+ case GST_STATE_CHANGE_NULL_TO_READY:
+ gst_hls_demux_reset (demux);
+ break;
+ default:
+ break;
+ }
+
+ ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
+ return ret;
+}
+
+static gboolean
+gst_hls_demux_sink_event (GstPad * pad, GstEvent * event)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (gst_pad_get_parent (pad));
+ GstQuery *query;
+ gchar *uri;
+
+
+ switch (event->type) {
+ case GST_EVENT_EOS:{
+ gchar *playlist;
+
+ if (demux->playlist == NULL) {
+ GST_WARNING_OBJECT (demux, "Received EOS without a playlist.");
+ break;
+ }
+
+ GST_DEBUG_OBJECT (demux, "Got EOS on the sink pad: main playlist fetched");
+
+ playlist = g_strndup ((gchar *) GST_BUFFER_DATA (demux->playlist),
+ GST_BUFFER_SIZE (demux->playlist));
+ gst_m3u8_client_update (demux->client, playlist);
+ gst_buffer_unref (demux->playlist);
+
+ query = gst_query_new_uri ();
+ if (gst_pad_peer_query (demux->sinkpad, query)) {
+ gst_query_parse_uri (query, &uri);
+ gst_hls_demux_set_location (demux, uri);
+ g_free (uri);
+ } else if (gst_m3u8_client_is_live (demux->client)) {
+ GST_ELEMENT_ERROR (demux, RESOURCE, NOT_FOUND,
+ ("Failed querying the playlist uri, "
+ "required for live sources."), NULL);
+ return FALSE;
+ }
+
+ gst_task_start (demux->task);
+ gst_event_unref (event);
+ return TRUE;
+ }
+ default:
+ break;
+ }
+
+ return gst_pad_event_default (pad, event);
+}
+
+static gboolean
+gst_hls_demux_fetcher_sink_event (GstPad * pad, GstEvent * event)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (gst_pad_get_element_private (pad));
+
+ switch (event->type) {
+ case GST_EVENT_EOS:{
+ GST_DEBUG_OBJECT (demux, "Got EOS on the fetcher pad");
+ /* signal we have fetched the URI */
+ g_cond_signal (demux->fetcher_cond);
+ }
+ default:
+ break;
+ }
+
+ gst_event_unref (event);
+ return FALSE;
+}
+
+static GstFlowReturn
+gst_hls_demux_chain (GstPad * pad, GstBuffer * buf)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (gst_pad_get_parent (pad));
+
+ if (demux->playlist == NULL) {
+ gst_buffer_ref (buf);
+ demux->playlist = buf;
+ } else {
+ demux->playlist = gst_buffer_join (demux->playlist, buf);
+ }
+
+ gst_object_unref (demux);
+
+ return GST_FLOW_OK;
+}
+
+static GstFlowReturn
+gst_hls_demux_fetcher_chain (GstPad * pad, GstBuffer * buf)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (gst_pad_get_element_private (pad));
+
+ /* The source element can be an http source element. In case we get a 404,
+ * the html response will be sent downstream and demux->downloaded_uri
+ * will not be null, which might make us think that the request proceed
+ * successfully. But it it will also post an error message in the bus that
+ * is handled synchronously and that will set demux->fetcher_error to TRUE,
+ * which is used to discard this buffer with the html response. */
+ if (demux->fetcher_error) {
+ goto done;
+ }
+
+ GST_LOG_OBJECT (demux, "Received new buffer in the fecther");
+ if (demux->downloaded_uri == NULL)
+ demux->downloaded_uri = buf;
+ else
+ demux->downloaded_uri = gst_buffer_join (demux->downloaded_uri, buf);
+
+done:
+ {
+ return GST_FLOW_OK;
+ }
+}
+
+static void
+gst_hls_demux_stop_fetcher (GstHLSDemux * demux, gboolean cancelled)
+{
+ GstPad *pad;
+
+ if (demux->fetcher == NULL)
+ return;
+
+ GST_DEBUG_OBJECT (demux, "Stopping fetcher.");
+ gst_element_set_state (demux->fetcher, GST_STATE_NULL);
+ pad = gst_pad_get_peer (demux->fetcherpad);
+ if (pad) {
+ gst_pad_unlink (pad, demux->fetcherpad);
+ gst_object_unref (pad);
+ }
+ gst_object_unref (demux->fetcher);
+ demux->fetcher = NULL;
+ if (cancelled && demux->downloaded_uri != NULL) {
+ gst_buffer_unref (demux->downloaded_uri);
+ demux->downloaded_uri = NULL;
+ g_cond_signal (demux->fetcher_cond);
+ }
+}
+
+static void
+gst_hls_demux_stop (GstHLSDemux * demux)
+{
+ gst_hls_demux_stop_fetcher (demux, TRUE);
+ if (GST_TASK_STATE (demux->task) != GST_TASK_STOPPED)
+ gst_task_stop (demux->task);
+ g_cond_signal (demux->thread_cond);
+}
+
+static void
+gst_hls_demux_loop (GstHLSDemux * demux)
+{
+ GstBuffer *buf;
+ GstFlowReturn ret;
+
+ /* cache the first fragments if we need it */
+ if (G_UNLIKELY (demux->need_cache)) {
+ gboolean ret;
+ ret = gst_hls_demux_cache_fragments (demux);
+ if (!ret) {
+ goto cache_error;
+ }
+ gst_hls_demux_start_update (demux);
+ GST_INFO_OBJECT (demux, "First fragments cached successfully");
+ }
+
+ if (g_queue_is_empty (demux->queue)) {
+ if (demux->end_of_playlist) {
+ goto end_of_playlist;
+ }
+ GST_TASK_WAIT (demux->task);
+ }
+
+ if (demux->end_of_playlist) {
+ goto end_of_playlist;
+ }
+
+ buf = g_queue_pop_head (demux->queue);
+ ret = gst_pad_push (demux->srcpad, buf);
+ if (ret != GST_FLOW_OK)
+ goto error;
+
+ return;
+
+end_of_playlist:
+ {
+ GST_DEBUG_OBJECT (demux, "Reached end of playlist, sending EOS");
+ gst_pad_push_event (demux->srcpad, gst_event_new_eos ());
+ gst_hls_demux_stop (demux);
+ return;
+ }
+
+cache_error:
+ {
+ GST_ELEMENT_ERROR (demux, RESOURCE, NOT_FOUND,
+ ("Could not cache the first fragments"), NULL);
+ gst_hls_demux_stop (demux);
+ return;
+ }
+
+error:
+ {
+ /* FIXME: handle error */
+ gst_hls_demux_stop (demux);
+ return;
+ }
+}
+
+static GstBusSyncReply
+gst_hls_demux_fetcher_bus_handler (GstBus * bus,
+ GstMessage * message, gpointer data)
+{
+ GstHLSDemux *demux = GST_HLS_DEMUX (data);
+
+ if (GST_MESSAGE_TYPE (message) == GST_MESSAGE_ERROR) {
+ demux->fetcher_error = TRUE;
+ g_cond_signal (demux->fetcher_cond);
+ }
+
+ gst_message_unref (message);
+ return GST_BUS_DROP;
+}
+
+static gboolean
+gst_hls_demux_make_fetcher (GstHLSDemux * demux, const gchar * uri)
+{
+ GstPad *pad;
+
+ if (!gst_uri_is_valid (uri))
+ return FALSE;
+
+ GST_DEBUG_OBJECT (demux, "Creating fetcher for the URI:%s", uri);
+ demux->fetcher = gst_element_make_from_uri (GST_URI_SRC, uri, NULL);
+ if (!demux->fetcher)
+ return FALSE;
+
+ demux->fetcher_error = FALSE;
+ gst_element_set_bus (GST_ELEMENT (demux->fetcher), demux->fetcher_bus);
+
+ g_object_set (G_OBJECT (demux->fetcher), "location", uri, NULL);
+ pad = gst_element_get_static_pad (demux->fetcher, "src");
+ if (pad) {
+ gst_pad_link (pad, demux->fetcherpad);
+ gst_object_unref (pad);
+ }
+ return TRUE;
+}
+
+static void
+gst_hls_demux_reset (GstHLSDemux * demux)
+{
+ demux->need_cache = TRUE;
+ demux->thread_return = FALSE;
+ demux->accumulated_delay = 0;
+ demux->end_of_playlist = FALSE;
+
+ if (demux->playlist) {
+ gst_buffer_unref (demux->playlist);
+ demux->playlist = NULL;
+ }
+
+ if (demux->downloaded_uri) {
+ gst_buffer_unref (demux->downloaded_uri);
+ demux->downloaded_uri = NULL;
+ }
+
+ if (demux->client)
+ gst_m3u8_client_free (demux->client);
+ demux->client = gst_m3u8_client_new ("");
+
+ while (!g_queue_is_empty (demux->queue)) {
+ GstBuffer *buf = g_queue_pop_head (demux->queue);
+ gst_buffer_unref (buf);
+ }
+ g_queue_clear (demux->queue);
+}
+
+static gboolean
+gst_hls_demux_set_location (GstHLSDemux * demux, const gchar * uri)
+{
+ if (demux->client)
+ gst_m3u8_client_free (demux->client);
+ demux->client = gst_m3u8_client_new (uri);
+ GST_INFO_OBJECT (demux, "Changed location: %s", uri);
+ return TRUE;
+}
+
+static gboolean
+gst_hls_demux_update_thread (GstHLSDemux * demux)
+{
+ g_mutex_lock (demux->thread_lock);
+ while (TRUE) {
+ /* block until the next scheduled update or the signal to quit this thread */
+ if (g_cond_timed_wait (demux->thread_cond, demux->thread_lock,
+ &demux->next_update)) {
+ goto quit;
+ }
+
+ /* update the playlist for live sources */
+ if (gst_m3u8_client_is_live (demux->client)) {
+ if (!gst_hls_demux_update_playlist (demux, TRUE)) {
+ GST_ERROR_OBJECT (demux, "Could not update the playlist");
+ goto quit;
+ }
+ }
+
+ /* schedule the next update */
+ gst_hls_demux_schedule (demux);
+
+ /* if it's a live source and the playlist couldn't be updated, there aren't
+ * more fragments in the playlist so we just wait for the next schedulled
+ * update */
+ if (gst_m3u8_client_is_live (demux->client) &&
+ demux->client->update_failed_count > 0) {
+ GST_WARNING_OBJECT (demux,
+ "The playlist hasn't been updated, failed count is %d",
+ demux->client->update_failed_count);
+ continue;
+ }
+
+ /* fetch the next fragment */
+ if (!gst_hls_demux_get_next_fragment (demux, TRUE)) {
+ if (!demux->end_of_playlist)
+ GST_ERROR_OBJECT (demux, "Could not fetch the next fragment");
+ goto quit;
+ }
+
+ /* try to switch to another bitrate if needed */
+ gst_hls_demux_switch_playlist (demux);
+ }
+
+quit:
+ {
+ g_mutex_unlock (demux->thread_lock);
+ return TRUE;
+ }
+}
+
+static gboolean
+gst_hls_demux_start_update (GstHLSDemux * demux)
+{
+ GError *error;
+
+ /* create a new thread for the updates so that we don't block in the streaming
+ * thread */
+ demux->updates_thread = g_thread_create (
+ (GThreadFunc) gst_hls_demux_update_thread, demux, TRUE, &error);
+ return (error != NULL);
+}
+
+static gboolean
+gst_hls_demux_cache_fragments (GstHLSDemux * demux)
+{
+ gint i;
+
+ /* Start parsing the main playlist */
+ gst_m3u8_client_set_current (demux->client, demux->client->main);
+
+ if (gst_m3u8_client_is_live (demux->client)) {
+ if (!gst_hls_demux_update_playlist (demux, FALSE)) {
+ GST_ERROR_OBJECT (demux, "Could not fetch the main playlist %s",
+ demux->client->main->uri);
+ return FALSE;
+ }
+ }
+
+ /* If this playlist is a list of playlists, select the first one
+ * and update it */
+ if (gst_m3u8_client_has_variant_playlist (demux->client)) {
+ GstM3U8 *child = demux->client->main->lists->data;
+ gst_m3u8_client_set_current (demux->client, child);
+ if (!gst_hls_demux_update_playlist (demux, FALSE)) {
+ GST_ERROR_OBJECT (demux, "Could not fetch the child playlist %s",
+ child->uri);
+ return FALSE;
+ }
+ }
+
+ /* If it's a live source, set the sequence number to the end of the list
+ * and substract the 'fragmets_cache' to start from the last fragment*/
+ if (gst_m3u8_client_is_live (demux->client)) {
+ demux->client->sequence += g_list_length (demux->client->current->files);
+ if (demux->client->sequence >= demux->fragments_cache)
+ demux->client->sequence -= demux->fragments_cache;
+ else
+ demux->client->sequence = 0;
+ }
+
+ /* Cache the first fragments */
+ for (i = 0; i < demux->fragments_cache - 1; i++) {
+ if (!gst_hls_demux_get_next_fragment (demux, FALSE)) {
+ GST_ERROR_OBJECT (demux, "Error caching the first fragments");
+ return FALSE;
+ }
+ }
+
+ g_get_current_time (&demux->next_update);
+
+ demux->need_cache = FALSE;
+ return TRUE;
+}
+
+static gboolean
+gst_hls_demux_fetch_location (GstHLSDemux * demux, const gchar * uri)
+{
+ GstStateChangeReturn ret;
+
+ g_mutex_lock (demux->fetcher_lock);
+
+ if (!gst_hls_demux_make_fetcher (demux, uri)) {
+ goto uri_error;
+ }
+
+ ret = gst_element_set_state (demux->fetcher, GST_STATE_PLAYING);
+ if (ret == GST_STATE_CHANGE_FAILURE)
+ goto state_change_error;
+
+ /* wait until we have fetched the element */
+ GST_DEBUG_OBJECT (demux, "Waiting to fetch the URI");
+ g_cond_wait (demux->fetcher_cond, demux->fetcher_lock);
+
+ gst_hls_demux_stop_fetcher (demux, FALSE);
+
+ if (demux->downloaded_uri != NULL) {
+ GST_INFO_OBJECT (demux, "URI fetched successfully");
+ ret = TRUE;
+ goto quit;
+ }
+ ret = FALSE;
+ goto quit;
+
+uri_error:
+ {
+ GST_ELEMENT_ERROR (demux, RESOURCE, OPEN_READ,
+ ("Could not create an element to fetch the given URI."), ("URI: \"%s\"",
+ uri));
+ ret = FALSE;
+ goto quit;
+ }
+
+state_change_error:
+ {
+ GST_ELEMENT_ERROR (demux, CORE, STATE_CHANGE,
+ ("Error changing state of the fetcher element."), NULL);
+ ret = FALSE;
+ goto quit;
+ }
+
+quit:
+ {
+ g_mutex_unlock (demux->fetcher_lock);
+ return ret;
+ }
+}
+
+static gboolean
+gst_hls_demux_update_playlist (GstHLSDemux * demux, gboolean retry)
+{
+ gchar *playlist;
+
+ GST_INFO_OBJECT (demux, "Updating the playlist %s", demux->client->current->uri);
+ if (!gst_hls_demux_fetch_location (demux, demux->client->current->uri))
+ return FALSE;
+
+ playlist = g_strndup ((gchar *) GST_BUFFER_DATA (demux->downloaded_uri),
+ GST_BUFFER_SIZE (demux->downloaded_uri));
+ gst_m3u8_client_update (demux->client, playlist);
+ gst_buffer_unref (demux->downloaded_uri);
+ demux->downloaded_uri = NULL;
+ return TRUE;
+}
+
+static gboolean
+gst_hls_demux_change_playlist (GstHLSDemux * demux, gboolean is_fast)
+{
+ if (is_fast) {
+ if (!demux->client->main->lists->next)
+ return TRUE;
+ demux->client->main->lists = g_list_next (demux->client->main->lists);
+ } else {
+ if (!demux->client->main->lists->prev)
+ return TRUE;
+ demux->client->main->lists = g_list_previous (demux->client->main->lists);
+ }
+
+ gst_m3u8_client_set_current (demux->client, demux->client->main->lists->data);
+ gst_hls_demux_update_playlist (demux, TRUE);
+ GST_INFO_OBJECT (demux, "Client is %s, switching to bitrate %d",
+ is_fast ? "fast" : "slow", demux->client->current->bandwidth);
+
+ return TRUE;
+}
+
+static gboolean
+gst_hls_demux_schedule (GstHLSDemux * demux)
+{
+ gfloat update_factor;
+ gint count;
+
+ /* As defined in §6.3.4. Reloading the Playlist file:
+ * "If the client reloads a Playlist file and finds that it has not
+ * changed then it MUST wait for a period of time before retrying. The
+ * minimum delay is a multiple of the target duration. This multiple is
+ * 0.5 for the first attempt, 1.5 for the second, and 3.0 thereafter."
+ */
+ count = demux->client->update_failed_count;
+ if (count < 3)
+ update_factor = update_interval_factor[count];
+ else
+ update_factor = update_interval_factor[3];
+
+ /* schedule the next update using the target duration field of the
+ * playlist */
+ g_time_val_add (&demux->next_update,
+ demux->client->current->targetduration * update_factor * 1000000);
+ GST_DEBUG_OBJECT (demux, "Next update scheduled at %s",
+ g_time_val_to_iso8601 (&demux->next_update));
+
+ return TRUE;
+}
+
+static gboolean
+gst_hls_demux_switch_playlist (GstHLSDemux * demux)
+{
+ GTimeVal now;
+ gint64 diff, limit;
+
+ g_get_current_time (&now);
+ if (!demux->client->main->lists)
+ return TRUE;
+
+ /* compare the time when the fragment was downloaded with the time when it was
+ * scheduled */
+ diff = (GST_TIMEVAL_TO_TIME (demux->next_update) - GST_TIMEVAL_TO_TIME (now));
+ limit = demux->client->current->targetduration * GST_SECOND *
+ demux->bitrate_switch_tol;
+
+ /* if we are on time switch to a higher bitrate */
+ if (diff > limit) {
+ gst_hls_demux_change_playlist (demux, TRUE);
+ } else if (diff < 0) {
+ /* if the client is to slow wait until it has accumulate a certain delay to
+ * switch to a lower bitrate */
+ demux->accumulated_delay -= diff;
+ if (demux->accumulated_delay >= limit) {
+ gst_hls_demux_change_playlist (demux, FALSE);
+ } else if (demux->accumulated_delay < 0) {
+ demux->accumulated_delay = 0;
+ }
+ }
+ return TRUE;
+}
+
+static gboolean
+gst_hls_demux_get_next_fragment (GstHLSDemux * demux, gboolean retry)
+{
+ const gchar *next_fragment_uri;
+ gboolean discont;
+
+ next_fragment_uri = gst_m3u8_client_get_next_fragment (demux->client, &discont);
+
+ if (!next_fragment_uri) {
+ GST_INFO_OBJECT (demux, "This playlist doesn't contain more fragments");
+ demux->end_of_playlist = TRUE;
+ GST_TASK_SIGNAL (demux->task);
+ return FALSE;
+ }
+
+ GST_INFO_OBJECT (demux, "Fetching next fragment %s", next_fragment_uri);
+
+ if (!gst_hls_demux_fetch_location (demux, next_fragment_uri))
+ return FALSE;
+
+ if (discont) {
+ GST_DEBUG_OBJECT (demux, "Marking fragment has discontinuous");
+ GST_BUFFER_FLAG_SET (demux->downloaded_uri, GST_BUFFER_FLAG_DISCONT);
+ }
+
+ g_queue_push_tail (demux->queue, demux->downloaded_uri);
+ GST_TASK_SIGNAL (demux->task);
+ demux->downloaded_uri = NULL;
+ return TRUE;
+}
--- /dev/null
+/* GStreamer
+ * Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
+ *
+ * m3u8.c:
+ *
+ * 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.
+ */
+
+#include <stdlib.h>
+#include <errno.h>
+#include <glib.h>
+
+#include "gstfragmented.h"
+#include "m3u8.h"
+
+#define GST_CAT_DEFAULT fragmented_debug
+
+static GstM3U8 *gst_m3u8_new (void);
+static void gst_m3u8_free (GstM3U8 * m3u8);
+static gboolean gst_m3u8_update (GstM3U8 * m3u8, gchar * data,
+ gboolean * updated);
+static GstM3U8MediaFile *gst_m3u8_media_file_new (gchar * uri,
+ gchar * title, gint duration, guint sequence);
+static void gst_m3u8_media_file_free (GstM3U8MediaFile * self);
+
+static GstM3U8 *
+gst_m3u8_new (void)
+{
+ GstM3U8 *m3u8;
+
+ m3u8 = g_new0 (GstM3U8, 1);
+
+ return m3u8;
+}
+
+static void
+gst_m3u8_set_uri (GstM3U8 * self, gchar * uri)
+{
+ g_return_if_fail (self != NULL);
+
+ if (self->uri)
+ g_free (self->uri);
+ self->uri = uri;
+}
+
+static void
+gst_m3u8_free (GstM3U8 * self)
+{
+ g_return_if_fail (self != NULL);
+
+ g_free (self->uri);
+ g_free (self->allowcache);
+ g_free (self->codecs);
+
+ g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_free, NULL);
+ g_list_free (self->files);
+
+ g_free (self->last_data);
+ g_list_foreach (self->lists, (GFunc) gst_m3u8_free, NULL);
+ g_list_free (self->lists);
+
+ g_free (self);
+}
+
+static GstM3U8MediaFile *
+gst_m3u8_media_file_new (gchar * uri, gchar * title, gint duration,
+ guint sequence)
+{
+ GstM3U8MediaFile *file;
+
+ file = g_new0 (GstM3U8MediaFile, 1);
+ file->uri = uri;
+ file->title = title;
+ file->duration = duration;
+ file->sequence = sequence;
+
+ return file;
+}
+
+static void
+gst_m3u8_media_file_free (GstM3U8MediaFile * self)
+{
+ g_return_if_fail (self != NULL);
+
+ g_free (self->title);
+ g_free (self->uri);
+ g_free (self);
+}
+
+static gboolean
+int_from_string (gchar * ptr, gchar ** endptr, gint * val)
+{
+ gchar *end;
+
+ g_return_val_if_fail (ptr != NULL, FALSE);
+ g_return_val_if_fail (val != NULL, FALSE);
+
+ errno = 0;
+ *val = strtol (ptr, &end, 10);
+ if ((errno == ERANGE && (*val == LONG_MAX || *val == LONG_MIN))
+ || (errno != 0 && *val == 0)) {
+ GST_WARNING (g_strerror (errno));
+ return FALSE;
+ }
+
+ if (endptr)
+ *endptr = end;
+
+ return end != ptr;
+}
+
+static gboolean
+parse_attributes (gchar ** ptr, gchar ** a, gchar ** v)
+{
+ gchar *end, *p;
+
+ g_return_val_if_fail (ptr != NULL, FALSE);
+ g_return_val_if_fail (*ptr != NULL, FALSE);
+ g_return_val_if_fail (a != NULL, FALSE);
+ g_return_val_if_fail (v != NULL, FALSE);
+
+ /* [attribute=value,]* */
+
+ *a = *ptr;
+ end = p = g_utf8_strchr (*ptr, -1, ',');
+ if (end) {
+ do {
+ end = g_utf8_next_char (end);
+ } while (end && *end == ' ');
+ *p = '\0';
+ }
+
+ *v = p = g_utf8_strchr (*ptr, -1, '=');
+ if (*v) {
+ *v = g_utf8_next_char (*v);
+ *p = '\0';
+ } else {
+ GST_WARNING ("missing = after attribute");
+ return FALSE;
+ }
+
+ *ptr = end;
+ return TRUE;
+}
+
+static gint
+_m3u8_compare_uri (GstM3U8 * a, gchar * uri)
+{
+ g_return_val_if_fail (a != NULL, 0);
+ g_return_val_if_fail (uri != NULL, 0);
+
+ return g_strcmp0 (a->uri, uri);
+}
+
+static gint
+gst_m3u8_compare_playlist_by_bitrate (gconstpointer a, gconstpointer b)
+{
+ return ((GstM3U8 *) (a))->bandwidth - ((GstM3U8 *) (b))->bandwidth;
+}
+
+/*
+ * @data: a m3u8 playlist text data, taking ownership
+ */
+static gboolean
+gst_m3u8_update (GstM3U8 * self, gchar * data, gboolean * updated)
+{
+ gint val, duration;
+ gchar *title, *end;
+ gboolean discontinuity;
+ GstM3U8 *list;
+
+ g_return_val_if_fail (self != NULL, FALSE);
+ g_return_val_if_fail (data != NULL, FALSE);
+ g_return_val_if_fail (updated != NULL, FALSE);
+
+ *updated = TRUE;
+
+ /* check if the data changed since last update */
+ if (self->last_data && g_str_equal (self->last_data, data)) {
+ GST_DEBUG ("Playlist is the same as previous one");
+ *updated = FALSE;
+ g_free (data);
+ return TRUE;
+ }
+
+ if (!g_str_has_prefix (data, "#EXTM3U")) {
+ GST_WARNING ("Data doesn't start with #EXTM3U");
+ g_free (data);
+ return FALSE;
+ }
+
+ g_free (self->last_data);
+ self->last_data = data;
+
+ list = NULL;
+ duration = -1;
+ title = NULL;
+ data += 7;
+ while (TRUE) {
+ end = g_utf8_strchr (data, -1, '\n'); /* FIXME: support \r\n */
+ if (end)
+ *end = '\0';
+
+ if (data[0] != '#') {
+ if (duration < 0 && list == NULL) {
+ GST_LOG ("%s: got line without EXTINF or EXTSTREAMINF, dropping", data);
+ goto next_line;
+ }
+
+ if (!gst_uri_is_valid (data)) {
+ gchar *slash;
+ if (!self->uri) {
+ GST_WARNING ("uri not set, can't build a valid uri");
+ goto next_line;
+ }
+ slash = g_utf8_strrchr (self->uri, -1, '/');
+ if (!slash) {
+ GST_WARNING ("Can't build a valid uri");
+ goto next_line;
+ }
+
+ *slash = '\0';
+ data = g_strdup_printf ("%s/%s", self->uri, data);
+ *slash = '/';
+ } else
+ data = g_strdup (data);
+
+ if (list != NULL) {
+ if (g_list_find_custom (self->lists, data,
+ (GCompareFunc) _m3u8_compare_uri)) {
+ GST_DEBUG ("Already have a list with this URI");
+ gst_m3u8_free (list);
+ g_free (data);
+ } else {
+ gst_m3u8_set_uri (list, data);
+ self->lists = g_list_append (self->lists, list);
+ }
+ list = NULL;
+ } else {
+ GstM3U8MediaFile *file;
+ file =
+ gst_m3u8_media_file_new (data, title, duration,
+ self->mediasequence++);
+ duration = -1;
+ title = NULL;
+ self->files = g_list_append (self->files, file);
+ }
+
+ } else if (g_str_has_prefix (data, "#EXT-X-ENDLIST")) {
+ self->endlist = TRUE;
+ } else if (g_str_has_prefix (data, "#EXT-X-VERSION:")) {
+ if (int_from_string (data + 15, &data, &val))
+ self->version = val;
+ } else if (g_str_has_prefix (data, "#EXT-X-STREAM-INF:")) {
+ gchar *v, *a;
+
+ if (list != NULL) {
+ GST_WARNING ("Found a list without a uri..., dropping");
+ gst_m3u8_free (list);
+ }
+
+ list = gst_m3u8_new ();
+ data = data + 18;
+ while (data && parse_attributes (&data, &a, &v)) {
+ if (g_str_equal (a, "BANDWIDTH")) {
+ if (!int_from_string (v, NULL, &list->bandwidth))
+ GST_WARNING ("Error while reading BANDWIDTH");
+ } else if (g_str_equal (a, "PROGRAM-ID")) {
+ if (!int_from_string (v, NULL, &list->program_id))
+ GST_WARNING ("Error while reading PROGRAM-ID");
+ } else if (g_str_equal (a, "CODECS")) {
+ g_free (list->codecs);
+ list->codecs = g_strdup (v);
+ } else if (g_str_equal (a, "RESOLUTION")) {
+ if (!int_from_string (v, &v, &list->width))
+ GST_WARNING ("Error while reading RESOLUTION width");
+ if (!v || *v != '=') {
+ GST_WARNING ("Missing height");
+ } else {
+ v = g_utf8_next_char (v);
+ if (!int_from_string (v, NULL, &list->height))
+ GST_WARNING ("Error while reading RESOLUTION height");
+ }
+ }
+ }
+ } else if (g_str_has_prefix (data, "#EXT-X-TARGETDURATION:")) {
+ if (int_from_string (data + 22, &data, &val))
+ self->targetduration = val;
+ } else if (g_str_has_prefix (data, "#EXT-X-MEDIA-SEQUENCE:")) {
+ if (int_from_string (data + 22, &data, &val))
+ self->mediasequence = val;
+ } else if (g_str_has_prefix (data, "#EXT-X-DISCONTINUITY")) {
+ discontinuity = TRUE;
+ } else if (g_str_has_prefix (data, "#EXT-X-PROGRAM-DATE-TIME:")) {
+ /* <YYYY-MM-DDThh:mm:ssZ> */
+ GST_DEBUG ("FIXME parse date");
+ } else if (g_str_has_prefix (data, "#EXT-X-ALLOW-CACHE:")) {
+ g_free (self->allowcache);
+ self->allowcache = g_strdup (data + 19);
+ } else if (g_str_has_prefix (data, "#EXTINF:")) {
+ if (!int_from_string (data + 8, &data, &val)) {
+ GST_WARNING ("Can't read EXTINF duration");
+ goto next_line;
+ }
+ duration = val;
+ if (duration > self->targetduration)
+ GST_WARNING ("EXTINF duration > TARGETDURATION");
+ if (!data || *data != ',')
+ goto next_line;
+ data = g_utf8_next_char (data);
+ if (data != end) {
+ g_free (title);
+ title = g_strdup (data);
+ }
+ } else {
+ GST_LOG ("Ignored line: %s", data);
+ }
+
+ next_line:
+ if (!end)
+ break;
+ data = g_utf8_next_char (end); /* skip \n */
+ }
+
+ /* redorder playlists by bitrate */
+ if (self->lists)
+ self->lists =
+ g_list_sort (self->lists,
+ (GCompareFunc) gst_m3u8_compare_playlist_by_bitrate);
+
+ return TRUE;
+}
+
+GstM3U8Client *
+gst_m3u8_client_new (const gchar * uri)
+{
+ GstM3U8Client *client;
+
+ g_return_val_if_fail (uri != NULL, NULL);
+
+ client = g_new0 (GstM3U8Client, 1);
+ client->main = gst_m3u8_new ();
+ client->current = NULL;
+ client->sequence = -1;
+ client->update_failed_count = 0;
+ gst_m3u8_set_uri (client->main, g_strdup (uri));
+
+ return client;
+}
+
+void
+gst_m3u8_client_free (GstM3U8Client * self)
+{
+ g_return_if_fail (self != NULL);
+
+ gst_m3u8_free (self->main);
+ g_free (self);
+}
+
+void
+gst_m3u8_client_set_current (GstM3U8Client * self, GstM3U8 * m3u8)
+{
+ g_return_if_fail (self != NULL);
+
+ if (m3u8 != self->current) {
+ self->current = m3u8;
+ self->update_failed_count = 0;
+ }
+}
+
+gboolean
+gst_m3u8_client_update (GstM3U8Client * self, gchar * data)
+{
+ GstM3U8 *m3u8;
+ gboolean updated = FALSE;
+
+ g_return_val_if_fail (self != NULL, FALSE);
+
+ m3u8 = self->current ? self->current : self->main;
+
+ if (!gst_m3u8_update (m3u8, data, &updated))
+ return FALSE;
+
+ if (!updated) {
+ self->update_failed_count++;
+ return FALSE;
+ }
+
+ /* select the first playlist, for now */
+ if (!self->current) {
+ if (self->main->lists) {
+ self->current = g_list_first (self->main->lists)->data;
+ } else {
+ self->current = self->main;
+ }
+ }
+
+ if (m3u8->files && self->sequence == -1) {
+ self->sequence =
+ GST_M3U8_MEDIA_FILE (g_list_first (m3u8->files)->data)->sequence;
+ GST_DEBUG ("Setting first sequence at %d", self->sequence);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+_find_next (GstM3U8MediaFile * file, GstM3U8Client * client)
+{
+ GST_DEBUG ("Found fragment %d", file->sequence);
+ if (file->sequence >= client->sequence)
+ return FALSE;
+ return TRUE;
+}
+
+const gchar *
+gst_m3u8_client_get_next_fragment (GstM3U8Client * client,
+ gboolean * discontinuity)
+{
+ GList *l;
+ GstM3U8MediaFile *file;
+
+ g_return_val_if_fail (client != NULL, NULL);
+ g_return_val_if_fail (client->current != NULL, NULL);
+ g_return_val_if_fail (discontinuity != NULL, NULL);
+
+ GST_DEBUG ("Looking for fragment %d", client->sequence);
+ l = g_list_find_custom (client->current->files, client,
+ (GCompareFunc) _find_next);
+ if (l == NULL)
+ return NULL;
+
+ file = GST_M3U8_MEDIA_FILE (l->data);
+
+ *discontinuity = client->sequence != file->sequence;
+ client->sequence = file->sequence + 1;
+
+ return file->uri;
+}