/* GStreamer * Copyright (C) 2010 Marc-Andre Lureau * * 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., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include "gstfragmented.h" #include "m3u8.h" #define GST_CAT_DEFAULT fragmented_debug #if !GLIB_CHECK_VERSION (2, 33, 4) #define g_list_copy_deep gst_g_list_copy_deep static GList * gst_g_list_copy_deep (GList * list, GCopyFunc func, gpointer user_data) { list = g_list_copy (list); if (func != NULL) { GList *l; for (l = list; l != NULL; l = l->next) { l->data = func (l->data, user_data); } } return list; } #endif static GstM3U8 *gst_m3u8_new (void); static void gst_m3u8_free (GstM3U8 * m3u8); static gboolean gst_m3u8_update (GstM3U8Client * client, GstM3U8 * m3u8, gchar * data, gboolean * updated); static GstM3U8MediaFile *gst_m3u8_media_file_new (gchar * uri, gchar * title, GstClockTime duration, guint sequence); static void gst_m3u8_media_file_free (GstM3U8MediaFile * self); gchar *uri_join (const gchar * uri, const gchar * path); 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, gchar * base_uri, gchar * name) { g_return_if_fail (self != NULL); g_free (self->uri); self->uri = uri; g_free (self->base_uri); self->base_uri = base_uri; g_free (self->name); self->name = name; } static void gst_m3u8_free (GstM3U8 * self) { g_return_if_fail (self != NULL); g_free (self->uri); g_free (self->base_uri); g_free (self->name); 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_list_foreach (self->iframe_lists, (GFunc) gst_m3u8_free, NULL); g_list_free (self->iframe_lists); g_free (self); } static GstM3U8MediaFile * gst_m3u8_media_file_new (gchar * uri, gchar * title, GstClockTime 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->key); g_free (self); } static GstM3U8MediaFile * gst_m3u8_media_file_copy (const GstM3U8MediaFile * self, gpointer user_data) { g_return_val_if_fail (self != NULL, NULL); return gst_m3u8_media_file_new (g_strdup (self->uri), g_strdup (self->title), self->duration, self->sequence); } static GstM3U8 * _m3u8_copy (const GstM3U8 * self, GstM3U8 * parent) { GstM3U8 *dup; g_return_val_if_fail (self != NULL, NULL); dup = gst_m3u8_new (); dup->uri = g_strdup (self->uri); dup->base_uri = g_strdup (self->base_uri); dup->name = g_strdup (self->name); dup->endlist = self->endlist; dup->version = self->version; dup->targetduration = self->targetduration; dup->allowcache = self->allowcache; dup->bandwidth = self->bandwidth; dup->program_id = self->program_id; dup->codecs = g_strdup (self->codecs); dup->width = self->width; dup->height = self->height; dup->iframe = self->iframe; dup->files = g_list_copy_deep (self->files, (GCopyFunc) gst_m3u8_media_file_copy, NULL); /* private */ dup->last_data = g_strdup (self->last_data); dup->lists = g_list_copy_deep (self->lists, (GCopyFunc) _m3u8_copy, dup); dup->iframe_lists = g_list_copy_deep (self->iframe_lists, (GCopyFunc) _m3u8_copy, dup); /* NOTE: current_variant will get set in gst_m3u8_copy () */ dup->parent = parent; dup->mediasequence = self->mediasequence; return dup; } static GstM3U8 * gst_m3u8_copy (const GstM3U8 * self) { GList *entry; guint n; GstM3U8 *dup = _m3u8_copy (self, NULL); if (self->current_variant != NULL) { for (n = 0, entry = self->lists; entry; entry = entry->next, n++) { if (entry == self->current_variant) { dup->current_variant = g_list_nth (dup->lists, n); break; } } if (!dup->current_variant) { for (n = 0, entry = self->iframe_lists; entry; entry = entry->next, n++) { if (entry == self->current_variant) { dup->current_variant = g_list_nth (dup->iframe_lists, n); break; } } if (!dup->current_variant) { GST_ERROR ("Failed to determine current playlist"); } } } return dup; } static gboolean int_from_string (gchar * ptr, gchar ** endptr, gint * val) { gchar *end; gint64 ret; g_return_val_if_fail (ptr != NULL, FALSE); g_return_val_if_fail (val != NULL, FALSE); errno = 0; ret = g_ascii_strtoll (ptr, &end, 10); if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64)) || (errno != 0 && ret == 0)) { GST_WARNING ("%s", g_strerror (errno)); return FALSE; } if (ret > G_MAXINT || ret < G_MININT) { GST_WARNING ("%s", g_strerror (ERANGE)); return FALSE; } if (endptr) *endptr = end; *val = (gint) ret; return end != ptr; } static gboolean int64_from_string (gchar * ptr, gchar ** endptr, gint64 * val) { gchar *end; gint64 ret; g_return_val_if_fail (ptr != NULL, FALSE); g_return_val_if_fail (val != NULL, FALSE); errno = 0; ret = g_ascii_strtoll (ptr, &end, 10); if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64)) || (errno != 0 && ret == 0)) { GST_WARNING ("%s", g_strerror (errno)); return FALSE; } if (endptr) *endptr = end; *val = ret; return end != ptr; } static gboolean double_from_string (gchar * ptr, gchar ** endptr, gdouble * val) { gchar *end; gdouble ret; g_return_val_if_fail (ptr != NULL, FALSE); g_return_val_if_fail (val != NULL, FALSE); errno = 0; ret = g_ascii_strtod (ptr, &end); if ((errno == ERANGE && (ret == HUGE_VAL || ret == -HUGE_VAL)) || (errno != 0 && ret == 0)) { GST_WARNING ("%s", g_strerror (errno)); return FALSE; } if (!isfinite (ret)) { GST_WARNING ("%s", g_strerror (ERANGE)); return FALSE; } if (endptr) *endptr = end; *val = (gdouble) ret; return end != ptr; } static gboolean parse_attributes (gchar ** ptr, gchar ** a, gchar ** v) { gchar *end = NULL, *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) { gchar *q = g_utf8_strchr (*ptr, -1, '"'); if (q && q < end) { /* special case, such as CODECS="avc1.77.30, mp4a.40.2" */ q = g_utf8_next_char (q); if (q) { q = g_utf8_strchr (q, -1, '"'); } if (q) { end = p = g_utf8_strchr (q, -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 gchar * unquote_string (gchar * string) { gchar *string_ret; string_ret = strchr (string, '"'); if (string_ret != NULL) { /* found initialization quotation mark of string */ string = string_ret + 1; string_ret = strchr (string, '"'); if (string_ret != NULL) { /* found finalizing quotation mark of string */ string_ret[0] = '\0'; } else { GST_WARNING ("wrong string unqouting - cannot find finalizing quotation mark"); return NULL; } } return string; } 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 (GstM3U8Client * client, GstM3U8 * self, gchar * data, gboolean * updated) { gint val; GstClockTime duration; gchar *title, *end; gboolean discontinuity = FALSE; GstM3U8 *list; gchar *current_key = NULL; gboolean have_iv = FALSE; guint8 iv[16] = { 0, }; gint64 size = -1, offset = -1; 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"); *updated = FALSE; g_free (data); return FALSE; } g_free (self->last_data); self->last_data = data; client->current_file = NULL; if (self->files) { g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_free, NULL); g_list_free (self->files); self->files = NULL; } client->duration = GST_CLOCK_TIME_NONE; /* By default, allow caching */ self->allowcache = TRUE; list = NULL; duration = 0; title = NULL; data += 7; while (TRUE) { gchar *r; end = g_utf8_strchr (data, -1, '\n'); if (end) *end = '\0'; r = g_utf8_strchr (data, -1, '\r'); if (r) *r = '\0'; if (data[0] != '#' && data[0] != '\0') { gchar *name = data; if (duration <= 0 && list == NULL) { GST_LOG ("%s: got line without EXTINF or EXTSTREAMINF, dropping", data); goto next_line; } data = uri_join (self->base_uri ? self->base_uri : self->uri, data); if (data == NULL) goto next_line; 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, NULL, g_strdup (name)); self->lists = g_list_append (self->lists, list); } list = NULL; } else { GstM3U8MediaFile *file; file = gst_m3u8_media_file_new (data, title, duration, self->mediasequence++); /* set encryption params */ file->key = current_key ? g_strdup (current_key) : NULL; if (file->key) { if (have_iv) { memcpy (file->iv, iv, sizeof (iv)); } else { guint8 *iv = file->iv + 12; GST_WRITE_UINT32_BE (iv, file->sequence); } } if (size != -1) { file->size = size; if (offset != -1) { file->offset = offset; } else { GstM3U8MediaFile *prev = self->files ? self->files->data : NULL; if (!prev) { offset = 0; } else { offset = prev->offset + prev->size; } file->offset = offset; } } else { file->size = -1; file->offset = 0; } file->discont = discontinuity; duration = 0; title = NULL; discontinuity = FALSE; size = offset = -1; self->files = g_list_prepend (self->files, file); } } else if (g_str_has_prefix (data, "#EXTINF:")) { gdouble fval; if (!double_from_string (data + 8, &data, &fval)) { GST_WARNING ("Can't read EXTINF duration"); goto next_line; } duration = fval * (gdouble) GST_SECOND; if (self->targetduration > 0 && duration > self->targetduration) { GST_WARNING ("EXTINF duration (%" GST_TIME_FORMAT ") > TARGETDURATION (%" GST_TIME_FORMAT ")", GST_TIME_ARGS (duration), GST_TIME_ARGS (self->targetduration)); } if (!data || *data != ',') goto next_line; data = g_utf8_next_char (data); if (data != end) { g_free (title); title = g_strdup (data); } } else if (g_str_has_prefix (data, "#EXT-X-")) { gchar *data_ext_x = data + 7; /* All these entries start with #EXT-X- */ 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:") || g_str_has_prefix (data_ext_x, "I-FRAME-STREAM-INF:")) { gchar *v, *a; gboolean iframe = g_str_has_prefix (data_ext_x, "I-FRAME-STREAM-INF:"); GstM3U8 *new_list; new_list = gst_m3u8_new (); new_list->parent = self; new_list->iframe = iframe; data = data + (iframe ? 26 : 18); while (data && parse_attributes (&data, &a, &v)) { if (g_str_equal (a, "BANDWIDTH")) { if (!int_from_string (v, NULL, &new_list->bandwidth)) GST_WARNING ("Error while reading BANDWIDTH"); } else if (g_str_equal (a, "PROGRAM-ID")) { if (!int_from_string (v, NULL, &new_list->program_id)) GST_WARNING ("Error while reading PROGRAM-ID"); } else if (g_str_equal (a, "CODECS")) { g_free (new_list->codecs); new_list->codecs = g_strdup (v); } else if (g_str_equal (a, "RESOLUTION")) { if (!int_from_string (v, &v, &new_list->width)) GST_WARNING ("Error while reading RESOLUTION width"); if (!v || *v != 'x') { GST_WARNING ("Missing height"); } else { v = g_utf8_next_char (v); if (!int_from_string (v, NULL, &new_list->height)) GST_WARNING ("Error while reading RESOLUTION height"); } } else if (iframe && g_str_equal (a, "URI")) { gchar *name; gchar *uri = g_strdup (v); gchar *urip = uri; uri = unquote_string (uri); if (uri) { uri = uri_join (self->base_uri ? self->base_uri : self->uri, uri); if (uri == NULL) { g_free (urip); continue; } name = g_strdup (uri); gst_m3u8_set_uri (new_list, uri, NULL, name); } else { GST_WARNING ("Cannot remove quotation marks from i-frame-stream URI"); } g_free (urip); } } if (iframe) { if (g_list_find_custom (self->iframe_lists, new_list->uri, (GCompareFunc) _m3u8_compare_uri)) { GST_DEBUG ("Already have a list with this URI"); gst_m3u8_free (new_list); } else { self->iframe_lists = g_list_append (self->iframe_lists, new_list); } } else if (list != NULL) { GST_WARNING ("Found a list without a uri..., dropping"); gst_m3u8_free (list); } else { list = new_list; } } else if (g_str_has_prefix (data_ext_x, "TARGETDURATION:")) { if (int_from_string (data + 22, &data, &val)) self->targetduration = val * GST_SECOND; } 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:")) { /* */ GST_DEBUG ("FIXME parse date"); } else if (g_str_has_prefix (data_ext_x, "ALLOW-CACHE:")) { self->allowcache = g_ascii_strcasecmp (data + 19, "YES") == 0; } else if (g_str_has_prefix (data_ext_x, "KEY:")) { gchar *v, *a; data = data + 11; /* IV and KEY are only valid until the next #EXT-X-KEY */ have_iv = FALSE; g_free (current_key); current_key = NULL; while (data && parse_attributes (&data, &a, &v)) { if (g_str_equal (a, "URI")) { gchar *key = g_strdup (v); gchar *keyp = key; key = unquote_string (key); if (key) { current_key = uri_join (self->base_uri ? self->base_uri : self->uri, key); } else { GST_WARNING ("Cannot remove quotation marks from decryption key URI"); } g_free (keyp); } else if (g_str_equal (a, "IV")) { gchar *ivp = v; gint i; if (strlen (ivp) < 32 + 2 || (!g_str_has_prefix (ivp, "0x") && !g_str_has_prefix (ivp, "0X"))) { GST_WARNING ("Can't read IV"); continue; } ivp += 2; for (i = 0; i < 16; i++) { gint h, l; h = g_ascii_xdigit_value (*ivp); ivp++; l = g_ascii_xdigit_value (*ivp); ivp++; if (h == -1 || l == -1) { i = -1; break; } iv[i] = (h << 4) | l; } if (i == -1) { GST_WARNING ("Can't read IV"); continue; } have_iv = TRUE; } else if (g_str_equal (a, "METHOD")) { if (!g_str_equal (v, "AES-128")) { GST_WARNING ("Encryption method %s not supported", v); continue; } } } } else if (g_str_has_prefix (data_ext_x, "BYTERANGE:")) { gchar *v = data + 17; if (int64_from_string (v, &v, &size)) { if (*v == '@' && !int64_from_string (v + 1, &v, &offset)) goto next_line; } else { goto next_line; } } else { GST_LOG ("Ignored line: %s", data); } } else { GST_LOG ("Ignored line: %s", data); } next_line: if (!end) break; data = g_utf8_next_char (end); /* skip \n */ } g_free (current_key); current_key = NULL; self->files = g_list_reverse (self->files); /* reorder playlists by bitrate */ if (self->lists) { gchar *top_variant_uri = NULL; gboolean iframe = FALSE; if (!self->current_variant) { top_variant_uri = GST_M3U8 (self->lists->data)->uri; } else { top_variant_uri = GST_M3U8 (self->current_variant->data)->uri; iframe = GST_M3U8 (self->current_variant->data)->iframe; } self->lists = g_list_sort (self->lists, (GCompareFunc) gst_m3u8_compare_playlist_by_bitrate); self->iframe_lists = g_list_sort (self->iframe_lists, (GCompareFunc) gst_m3u8_compare_playlist_by_bitrate); if (iframe) self->current_variant = g_list_find_custom (self->iframe_lists, top_variant_uri, (GCompareFunc) _m3u8_compare_uri); else self->current_variant = g_list_find_custom (self->lists, top_variant_uri, (GCompareFunc) _m3u8_compare_uri); } /* calculate the start and end times of this media playlist. */ if (self->files) { GList *walk; GstM3U8MediaFile *file; GstClockTime duration = 0; for (walk = self->files; walk; walk = walk->next) { file = walk->data; duration += file->duration; if (file->sequence > client->highest_sequence_number) { if (client->highest_sequence_number >= 0) { /* if an update of the media playlist has been missed, there will be a gap between self->highest_sequence_number and the first sequence number in this media playlist. In this situation assume that the missing fragments had a duration of targetduration each */ client->last_file_end += (file->sequence - client->highest_sequence_number - 1) * self->targetduration; } client->last_file_end += file->duration; client->highest_sequence_number = file->sequence; } } if (GST_M3U8_CLIENT_IS_LIVE (client)) { client->first_file_start = client->last_file_end - duration; GST_DEBUG ("Live playlist range %" GST_TIME_FORMAT " -> %" GST_TIME_FORMAT, GST_TIME_ARGS (client->first_file_start), GST_TIME_ARGS (client->last_file_end)); } client->duration = duration; } return TRUE; } GstM3U8Client * gst_m3u8_client_new (const gchar * uri, const gchar * base_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->current_file = NULL; client->sequence = -1; client->sequence_position = 0; client->update_failed_count = 0; client->highest_sequence_number = -1; client->duration = GST_CLOCK_TIME_NONE; g_mutex_init (&client->lock); gst_m3u8_set_uri (client->main, g_strdup (uri), g_strdup (base_uri), NULL); return client; } void gst_m3u8_client_free (GstM3U8Client * self) { g_return_if_fail (self != NULL); gst_m3u8_free (self->main); g_mutex_clear (&self->lock); g_free (self); } void gst_m3u8_client_set_current (GstM3U8Client * self, GstM3U8 * m3u8) { g_return_if_fail (self != NULL); GST_M3U8_CLIENT_LOCK (self); if (m3u8 != self->current) { self->current = m3u8; self->update_failed_count = 0; self->duration = GST_CLOCK_TIME_NONE; self->current_file = NULL; } GST_M3U8_CLIENT_UNLOCK (self); } gboolean gst_m3u8_client_update (GstM3U8Client * self, gchar * data) { GstM3U8 *m3u8; gboolean updated = FALSE; gboolean ret = FALSE; g_return_val_if_fail (self != NULL, FALSE); GST_M3U8_CLIENT_LOCK (self); m3u8 = self->current ? self->current : self->main; if (!gst_m3u8_update (self, m3u8, data, &updated)) goto out; if (!updated) { self->update_failed_count++; goto out; } if (self->current && !self->current->files) { GST_ERROR ("Invalid media playlist, it does not contain any media files"); goto out; } /* select the first playlist, for now */ if (!self->current) { if (self->main->lists) { self->current = self->main->current_variant->data; } else { self->current = self->main; } } if (m3u8->files && self->sequence == -1) { self->current_file = g_list_first (m3u8->files); if (GST_M3U8_CLIENT_IS_LIVE (self)) { /* for live streams, start GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE from the end of the playlist. See section 6.3.3 of HLS draft */ gint pos = g_list_length (m3u8->files) - GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE; self->sequence = GST_M3U8_MEDIA_FILE (g_list_nth_data (m3u8->files, pos >= 0 ? pos : 0))->sequence; } else self->sequence = GST_M3U8_MEDIA_FILE (self->current_file->data)->sequence; self->sequence_position = 0; GST_DEBUG ("Setting first sequence at %u", (guint) self->sequence); } ret = TRUE; out: GST_M3U8_CLIENT_UNLOCK (self); return ret; } static gint _find_m3u8_list_match (const GstM3U8 * a, const GstM3U8 * b) { if (g_strcmp0 (a->name, b->name) == 0 && a->bandwidth == b->bandwidth && a->program_id == b->program_id && g_strcmp0 (a->codecs, b->codecs) == 0 && a->width == b->width && a->height == b->height && a->iframe == b->iframe) { return 0; } return 1; } gboolean gst_m3u8_client_update_variant_playlist (GstM3U8Client * self, gchar * data, const gchar * uri, const gchar * base_uri) { gboolean ret = FALSE; GList *list_entry, *unmatched_lists; GstM3U8Client *new_client; GstM3U8 *old; g_return_val_if_fail (self != NULL, FALSE); new_client = gst_m3u8_client_new (uri, base_uri); if (gst_m3u8_client_update (new_client, data)) { if (!new_client->main->lists) { GST_ERROR ("Cannot update variant playlist: New playlist is not a variant playlist"); gst_m3u8_client_free (new_client); return FALSE; } GST_M3U8_CLIENT_LOCK (self); if (!self->main->lists) { GST_ERROR ("Cannot update variant playlist: Current playlist is not a variant playlist"); goto out; } /* Now see if the variant playlist still has the same lists */ unmatched_lists = g_list_copy (self->main->lists); for (list_entry = new_client->main->lists; list_entry; list_entry = list_entry->next) { GList *match = g_list_find_custom (unmatched_lists, list_entry->data, (GCompareFunc) _find_m3u8_list_match); if (match) unmatched_lists = g_list_remove_link (unmatched_lists, match); } if (unmatched_lists != NULL) { GST_WARNING ("Unable to match all playlists"); for (list_entry = unmatched_lists; list_entry; list_entry = list_entry->next) { if (list_entry->data == self->current) { GST_WARNING ("Unable to match current playlist"); } } g_list_free (unmatched_lists); } /* Switch out the variant playlist */ old = self->main; self->main = gst_m3u8_copy (new_client->main); if (self->main->lists) self->current = self->main->current_variant->data; else self->current = self->main; gst_m3u8_free (old); ret = TRUE; out: GST_M3U8_CLIENT_UNLOCK (self); } gst_m3u8_client_free (new_client); return ret; } static gboolean _find_current (GstM3U8MediaFile * file, GstM3U8Client * client) { return file->sequence != client->sequence; } static GList * find_next_fragment (GstM3U8Client * client, GList * l, gboolean forward) { GstM3U8MediaFile *file; if (forward) { while (l) { file = l->data; if (file->sequence >= client->sequence) break; l = l->next; } } else { l = g_list_last (l); while (l) { file = l->data; if (file->sequence <= client->sequence) break; l = l->prev; } } return l; } static gboolean has_next_fragment (GstM3U8Client * client, GList * l, gboolean forward) { l = find_next_fragment (client, l, forward); if (l) { return (forward && l->next) || (!forward && l->prev); } return FALSE; } gboolean gst_m3u8_client_get_next_fragment (GstM3U8Client * client, gboolean * discontinuity, gchar ** uri, GstClockTime * duration, GstClockTime * timestamp, gint64 * range_start, gint64 * range_end, gchar ** key, guint8 ** iv, gboolean forward) { GstM3U8MediaFile *file; g_return_val_if_fail (client != NULL, FALSE); g_return_val_if_fail (client->current != NULL, FALSE); GST_M3U8_CLIENT_LOCK (client); GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, client->sequence); if (client->sequence < 0) { GST_M3U8_CLIENT_UNLOCK (client); return FALSE; } if (!client->current_file) { client->current_file = find_next_fragment (client, client->current->files, forward); } if (!client->current_file) { GST_M3U8_CLIENT_UNLOCK (client); return FALSE; } file = GST_M3U8_MEDIA_FILE (client->current_file->data); GST_DEBUG ("Got fragment with sequence %u (client sequence %u)", (guint) file->sequence, (guint) client->sequence); if (timestamp) *timestamp = client->sequence_position; if (discontinuity) *discontinuity = client->sequence != file->sequence || file->discont; if (uri) *uri = g_strdup (file->uri); if (duration) *duration = file->duration; if (range_start) *range_start = file->offset; if (range_end) *range_end = file->size != -1 ? file->offset + file->size - 1 : -1; if (key) *key = g_strdup (file->key); if (iv) { *iv = g_new (guint8, sizeof (file->iv)); memcpy (*iv, file->iv, sizeof (file->iv)); } client->sequence = file->sequence; GST_M3U8_CLIENT_UNLOCK (client); return TRUE; } gboolean gst_m3u8_client_has_next_fragment (GstM3U8Client * client, gboolean forward) { gboolean ret; g_return_val_if_fail (client != NULL, FALSE); g_return_val_if_fail (client->current != NULL, FALSE); GST_M3U8_CLIENT_LOCK (client); GST_DEBUG ("Checking if has next fragment %" G_GINT64_FORMAT, client->sequence + (forward ? 1 : -1)); if (client->current_file) { ret = (forward ? client->current_file->next : client->current_file->prev) != NULL; } else { ret = has_next_fragment (client, client->current->files, forward); } GST_M3U8_CLIENT_UNLOCK (client); return ret; } static void alternate_advance (GstM3U8Client * client, gboolean forward) { gint targetnum = client->sequence; GList *tmp; GstM3U8MediaFile *mf; /* figure out the target seqnum */ if (forward) targetnum += 1; else targetnum -= 1; for (tmp = client->current->files; tmp; tmp = tmp->next) { mf = (GstM3U8MediaFile *) tmp->data; if (mf->sequence == targetnum) break; } if (tmp == NULL) { GST_WARNING ("Can't find next fragment"); return; } client->current_file = tmp; client->sequence = targetnum; if (forward) client->sequence_position += mf->duration; else { if (client->sequence_position > mf->duration) client->sequence_position -= mf->duration; else client->sequence_position = 0; } } void gst_m3u8_client_advance_fragment (GstM3U8Client * client, gboolean forward) { GstM3U8MediaFile *file; g_return_if_fail (client != NULL); g_return_if_fail (client->current != NULL); GST_M3U8_CLIENT_LOCK (client); if (!client->current_file) { GList *l; GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, client->sequence); l = g_list_find_custom (client->current->files, client, (GCompareFunc) _find_current); if (l == NULL) { GST_DEBUG ("Could not find current fragment, trying next fragment directly"); alternate_advance (client, forward); GST_M3U8_CLIENT_UNLOCK (client); return; } client->current_file = l; } file = GST_M3U8_MEDIA_FILE (client->current_file->data); GST_DEBUG ("Advancing from sequence %u", (guint) file->sequence); if (forward) { client->current_file = client->current_file->next; if (client->current_file) { client->sequence = GST_M3U8_MEDIA_FILE (client->current_file->data)->sequence; } else { client->sequence = file->sequence + 1; } client->sequence_position += file->duration; } else { client->current_file = client->current_file->prev; if (client->current_file) { client->sequence = GST_M3U8_MEDIA_FILE (client->current_file->data)->sequence; } else { client->sequence = file->sequence - 1; } if (client->sequence_position > file->duration) client->sequence_position -= file->duration; else client->sequence_position = 0; } GST_M3U8_CLIENT_UNLOCK (client); } static void _sum_duration (GstM3U8MediaFile * self, GstClockTime * duration) { *duration += self->duration; } GstClockTime gst_m3u8_client_get_duration (GstM3U8Client * client) { GstClockTime duration = GST_CLOCK_TIME_NONE; g_return_val_if_fail (client != NULL, GST_CLOCK_TIME_NONE); GST_M3U8_CLIENT_LOCK (client); /* We can only get the duration for on-demand streams */ if (!client->current || !client->current->endlist) { GST_M3U8_CLIENT_UNLOCK (client); return GST_CLOCK_TIME_NONE; } if (!GST_CLOCK_TIME_IS_VALID (client->duration) && client->current->files) { client->duration = 0; g_list_foreach (client->current->files, (GFunc) _sum_duration, &client->duration); } duration = client->duration; GST_M3U8_CLIENT_UNLOCK (client); return duration; } GstClockTime gst_m3u8_client_get_target_duration (GstM3U8Client * client) { GstClockTime duration = 0; g_return_val_if_fail (client != NULL, GST_CLOCK_TIME_NONE); GST_M3U8_CLIENT_LOCK (client); duration = client->current->targetduration; GST_M3U8_CLIENT_UNLOCK (client); return duration; } gchar * gst_m3u8_client_get_uri (GstM3U8Client * client) { gchar *uri; g_return_val_if_fail (client != NULL, NULL); GST_M3U8_CLIENT_LOCK (client); uri = client->main ? g_strdup (client->main->uri) : NULL; GST_M3U8_CLIENT_UNLOCK (client); return uri; } gchar * gst_m3u8_client_get_current_uri (GstM3U8Client * client) { gchar *uri; g_return_val_if_fail (client != NULL, NULL); GST_M3U8_CLIENT_LOCK (client); uri = g_strdup (client->current->uri); GST_M3U8_CLIENT_UNLOCK (client); return uri; } gboolean gst_m3u8_client_has_main (GstM3U8Client * client) { gboolean ret; g_return_val_if_fail (client != NULL, FALSE); GST_M3U8_CLIENT_LOCK (client); if (client->main) ret = TRUE; else ret = FALSE; GST_M3U8_CLIENT_UNLOCK (client); return ret; } gboolean gst_m3u8_client_has_variant_playlist (GstM3U8Client * client) { gboolean ret; g_return_val_if_fail (client != NULL, FALSE); GST_M3U8_CLIENT_LOCK (client); ret = (client->main->lists != NULL); GST_M3U8_CLIENT_UNLOCK (client); return ret; } gboolean gst_m3u8_client_is_live (GstM3U8Client * client) { gboolean ret; g_return_val_if_fail (client != NULL, FALSE); GST_M3U8_CLIENT_LOCK (client); ret = GST_M3U8_CLIENT_IS_LIVE (client); GST_M3U8_CLIENT_UNLOCK (client); return ret; } GList * gst_m3u8_client_get_playlist_for_bitrate (GstM3U8Client * client, guint bitrate) { GList *list, *current_variant; GST_M3U8_CLIENT_LOCK (client); current_variant = client->main->current_variant; /* Go to the highest possible bandwidth allowed */ while (GST_M3U8 (current_variant->data)->bandwidth <= bitrate) { list = g_list_next (current_variant); if (!list) break; current_variant = list; } while (GST_M3U8 (current_variant->data)->bandwidth > bitrate) { list = g_list_previous (current_variant); if (!list) break; current_variant = list; } GST_M3U8_CLIENT_UNLOCK (client); return current_variant; } gchar * uri_join (const gchar * uri1, const gchar * uri2) { gchar *uri_copy, *tmp, *ret = NULL; if (gst_uri_is_valid (uri2)) return g_strdup (uri2); uri_copy = g_strdup (uri1); if (uri2[0] != '/') { /* uri2 is a relative uri2 */ /* look for query params */ tmp = g_utf8_strchr (uri_copy, -1, '?'); if (tmp) { /* find last / char, ignoring query params */ tmp = g_utf8_strrchr (uri_copy, tmp - uri_copy, '/'); } else { /* find last / char in URL */ tmp = g_utf8_strrchr (uri_copy, -1, '/'); } if (!tmp) { GST_WARNING ("Can't build a valid uri_copy"); goto out; } *tmp = '\0'; ret = g_strdup_printf ("%s/%s", uri_copy, uri2); } else { /* uri2 is an absolute uri2 */ char *scheme, *hostname; scheme = uri_copy; /* find the : in :// */ tmp = g_utf8_strchr (uri_copy, -1, ':'); if (!tmp) { GST_WARNING ("Can't build a valid uri_copy"); goto out; } *tmp = '\0'; /* skip :// */ hostname = tmp + 3; tmp = g_utf8_strchr (hostname, -1, '/'); if (tmp) *tmp = '\0'; ret = g_strdup_printf ("%s://%s%s", scheme, hostname, uri2); } out: g_free (uri_copy); return ret; } guint64 gst_m3u8_client_get_current_fragment_duration (GstM3U8Client * client) { guint64 dur; GList *list; g_return_val_if_fail (client != NULL, 0); GST_M3U8_CLIENT_LOCK (client); list = g_list_find_custom (client->current->files, client, (GCompareFunc) _find_current); if (list == NULL) { dur = -1; } else { dur = GST_M3U8_MEDIA_FILE (list->data)->duration; } GST_M3U8_CLIENT_UNLOCK (client); return dur; } gboolean gst_m3u8_client_get_seek_range (GstM3U8Client * client, gint64 * start, gint64 * stop) { GstClockTime duration = 0; GList *walk; GstM3U8MediaFile *file; guint count; g_return_val_if_fail (client != NULL, FALSE); GST_M3U8_CLIENT_LOCK (client); if (client->current == NULL || client->current->files == NULL) { GST_M3U8_CLIENT_UNLOCK (client); return FALSE; } count = g_list_length (client->current->files); /* count is used to make sure the seek range is never closer than GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE fragments from the end of the playlist - see 6.3.3. "Playing the Playlist file" of the HLS draft */ for (walk = client->current->files; walk && count >= GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE; walk = walk->next) { file = walk->data; --count; duration += file->duration; } if (duration <= 0) { GST_M3U8_CLIENT_UNLOCK (client); return FALSE; } *start = client->first_file_start; *stop = *start + duration; GST_M3U8_CLIENT_UNLOCK (client); return TRUE; }