summaryrefslogtreecommitdiff
path: root/gst/hls/m3u8.c
diff options
context:
space:
mode:
authorAndoni Morales Alastruey <amorales@flumotion.com>2011-02-14 18:51:32 +0100
committerSebastian Dröge <sebastian.droege@collabora.co.uk>2011-03-30 09:19:20 +0200
commit9aff2de6253419909226416133f24bd05abcf12d (patch)
tree9dcd2eb00540c64ce4b3648f9b302edc21590235 /gst/hls/m3u8.c
parentdfe64965d79a09c5c65f3a47a2c5673b6cf825f3 (diff)
downloadgstreamer-plugins-bad-9aff2de6253419909226416133f24bd05abcf12d.tar.gz
hlsdemux: Add HTTP live streaming demuxer element
Based on previous work by Marc-André Lureau
Diffstat (limited to 'gst/hls/m3u8.c')
-rw-r--r--gst/hls/m3u8.c452
1 files changed, 452 insertions, 0 deletions
diff --git a/gst/hls/m3u8.c b/gst/hls/m3u8.c
new file mode 100644
index 000000000..ea5aa6859
--- /dev/null
+++ b/gst/hls/m3u8.c
@@ -0,0 +1,452 @@
+/* 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;
+}