diff options
author | Andoni Morales Alastruey <amorales@flumotion.com> | 2011-02-14 18:51:32 +0100 |
---|---|---|
committer | Sebastian Dröge <sebastian.droege@collabora.co.uk> | 2011-03-30 09:19:20 +0200 |
commit | 9aff2de6253419909226416133f24bd05abcf12d (patch) | |
tree | 9dcd2eb00540c64ce4b3648f9b302edc21590235 /gst/hls/m3u8.c | |
parent | dfe64965d79a09c5c65f3a47a2c5673b6cf825f3 (diff) | |
download | gstreamer-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.c | 452 |
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; +} |