2 * Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
3 * Copyright (C) 2015 Tim-Philipp Müller <tim@centricular.com>
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Library General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Library General Public License for more details.
17 * You should have received a copy of the GNU Library General Public
18 * License along with this library; if not, write to the
19 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
20 * Boston, MA 02110-1301, USA.
32 #define GST_CAT_DEFAULT hls_debug
34 static GstM3U8MediaFile *gst_m3u8_media_file_new (gchar * uri,
35 gchar * title, GstClockTime duration, guint sequence);
36 static gchar *uri_join (const gchar * uri, const gchar * path);
43 m3u8 = g_new0 (GstM3U8, 1);
45 m3u8->current_file = NULL;
46 m3u8->current_file_duration = GST_CLOCK_TIME_NONE;
48 m3u8->sequence_position = 0;
49 m3u8->highest_sequence_number = -1;
50 m3u8->duration = GST_CLOCK_TIME_NONE;
52 g_mutex_init (&m3u8->lock);
58 /* call with M3U8_LOCK held */
60 gst_m3u8_take_uri (GstM3U8 * self, gchar * uri, gchar * base_uri, gchar * name)
62 g_return_if_fail (self != NULL);
64 if (self->uri != uri) {
68 if (self->base_uri != base_uri) {
69 g_free (self->base_uri);
70 self->base_uri = base_uri;
72 if (self->name != name) {
79 gst_m3u8_set_uri (GstM3U8 * m3u8, const gchar * uri, const gchar * base_uri,
83 gst_m3u8_take_uri (m3u8, g_strdup (uri), g_strdup (base_uri),
85 GST_M3U8_UNLOCK (m3u8);
89 gst_m3u8_ref (GstM3U8 * m3u8)
91 g_assert (m3u8 != NULL && m3u8->ref_count > 0);
93 g_atomic_int_add (&m3u8->ref_count, 1);
98 gst_m3u8_unref (GstM3U8 * self)
100 g_return_if_fail (self != NULL && self->ref_count > 0);
102 if (g_atomic_int_dec_and_test (&self->ref_count)) {
104 g_free (self->base_uri);
107 g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_unref, NULL);
108 g_list_free (self->files);
110 g_free (self->last_data);
115 static GstM3U8MediaFile *
116 gst_m3u8_media_file_new (gchar * uri, gchar * title, GstClockTime duration,
119 GstM3U8MediaFile *file;
121 file = g_new0 (GstM3U8MediaFile, 1);
124 file->duration = duration;
125 file->sequence = sequence;
132 gst_m3u8_media_file_ref (GstM3U8MediaFile * mfile)
134 g_assert (mfile != NULL && mfile->ref_count > 0);
136 g_atomic_int_add (&mfile->ref_count, 1);
141 gst_m3u8_media_file_unref (GstM3U8MediaFile * self)
143 g_return_if_fail (self != NULL && self->ref_count > 0);
145 if (g_atomic_int_dec_and_test (&self->ref_count)) {
146 g_free (self->title);
154 int_from_string (gchar * ptr, gchar ** endptr, gint * val)
159 g_return_val_if_fail (ptr != NULL, FALSE);
160 g_return_val_if_fail (val != NULL, FALSE);
163 ret = g_ascii_strtoll (ptr, &end, 10);
164 if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64))
165 || (errno != 0 && ret == 0)) {
166 GST_WARNING ("%s", g_strerror (errno));
170 if (ret > G_MAXINT || ret < G_MININT) {
171 GST_WARNING ("%s", g_strerror (ERANGE));
184 int64_from_string (gchar * ptr, gchar ** endptr, gint64 * val)
189 g_return_val_if_fail (ptr != NULL, FALSE);
190 g_return_val_if_fail (val != NULL, FALSE);
193 ret = g_ascii_strtoll (ptr, &end, 10);
194 if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64))
195 || (errno != 0 && ret == 0)) {
196 GST_WARNING ("%s", g_strerror (errno));
209 double_from_string (gchar * ptr, gchar ** endptr, gdouble * val)
214 g_return_val_if_fail (ptr != NULL, FALSE);
215 g_return_val_if_fail (val != NULL, FALSE);
218 ret = g_ascii_strtod (ptr, &end);
219 if ((errno == ERANGE && (ret == HUGE_VAL || ret == -HUGE_VAL))
220 || (errno != 0 && ret == 0)) {
221 GST_WARNING ("%s", g_strerror (errno));
225 if (!isfinite (ret)) {
226 GST_WARNING ("%s", g_strerror (ERANGE));
233 *val = (gdouble) ret;
239 parse_attributes (gchar ** ptr, gchar ** a, gchar ** v)
241 gchar *end = NULL, *p, *ve;
243 g_return_val_if_fail (ptr != NULL, FALSE);
244 g_return_val_if_fail (*ptr != NULL, FALSE);
245 g_return_val_if_fail (a != NULL, FALSE);
246 g_return_val_if_fail (v != NULL, FALSE);
248 /* [attribute=value,]* */
251 end = p = g_utf8_strchr (*ptr, -1, ',');
253 gchar *q = g_utf8_strchr (*ptr, -1, '"');
255 /* special case, such as CODECS="avc1.77.30, mp4a.40.2" */
256 q = g_utf8_next_char (q);
258 q = g_utf8_strchr (q, -1, '"');
261 end = p = g_utf8_strchr (q, -1, ',');
267 end = g_utf8_next_char (end);
268 } while (end && *end == ' ');
272 *v = p = g_utf8_strchr (*ptr, -1, '=');
275 *v = g_utf8_next_char (*v);
277 ve = g_utf8_next_char (*v);
279 ve = g_utf8_strchr (ve, -1, '"');
282 *v = g_utf8_next_char (*v);
285 GST_WARNING ("Cannot remove quotation marks from %s", *a);
289 GST_WARNING ("missing = after attribute");
298 gst_hls_variant_stream_compare_by_bitrate (gconstpointer a, gconstpointer b)
300 const GstHLSVariantStream *vs_a = (const GstHLSVariantStream *) a;
301 const GstHLSVariantStream *vs_b = (const GstHLSVariantStream *) b;
303 if (vs_a->bandwidth == vs_b->bandwidth)
304 return g_strcmp0 (vs_a->name, vs_b->name);
306 return vs_a->bandwidth - vs_b->bandwidth;
310 gst_m3u8_update_check_consistent_media_seqnums (GstM3U8 * self,
311 gboolean have_mediasequence, GList * previous_files)
316 /* If we have MEDIA-SEQUENCE, ensure that it's consistent. If it is not,
317 * the client SHOULD halt playback (6.3.4), which is what we do then.
319 * If we don't have MEDIA-SEQUENCE, we check URIs in the previous and
320 * current playlist to calculate the/a correct MEDIA-SEQUENCE for the new
321 * playlist in relation to the old. That is, same URIs get the same number
322 * and later URIs get higher numbers */
323 if (have_mediasequence) {
325 GstM3U8MediaFile *f1 = NULL, *f2 = NULL;
327 /* Find first case of higher/equal sequence number in new playlist or
328 * same URI. From there on we can linearly step ahead */
329 for (l = self->files; l; l = l->next) {
330 gboolean match = FALSE;
333 for (m = previous_files; m; m = m->next) {
336 if (f1->sequence >= f2->sequence || g_str_equal (f1->uri, f2->uri)) {
346 /* No match, no sequence in the new playlist was higher than
347 * any in the old, and no URI was found again. This is bad! */
348 GST_ERROR ("Media sequences inconsistent, ignoring");
351 g_assert (f1 != NULL);
352 g_assert (f2 != NULL);
354 for (; l && m; l = l->next, m = m->next) {
358 if (f1->sequence == f2->sequence) {
359 if (!g_str_equal (f1->uri, f2->uri)) {
360 /* Same sequence, different URI. This is bad! */
361 GST_ERROR ("Media sequences inconsistent, ignoring");
364 /* Good case, we advance and check the next one */
366 } else if (g_str_equal (f1->uri, f2->uri)) {
367 /* Same URIs but different sequences, this is bad! */
368 GST_ERROR ("Media sequences inconsistent, ignoring");
371 /* Not same URI, not same sequence but by construction sequence
372 * must be higher in the new one. All good in that case, if it
373 * isn't then this means that sequence numbers are decreasing
374 * or files were inserted */
375 if (f1->sequence < f2->sequence) {
376 GST_ERROR ("Media sequences inconsistent, ignoring");
382 /* All good if we're getting here */
386 GstM3U8MediaFile *f1 = NULL, *f2 = NULL;
387 gint64 mediasequence;
389 for (l = self->files; l; l = l->next) {
390 gboolean match = FALSE;
393 for (m = previous_files; m; m = m->next) {
396 if (g_str_equal (f1->uri, f2->uri)) {
407 /* No match, this means f2 is the last item in the previous playlist
408 * and we have to start our new playlist at that sequence */
409 mediasequence = f2->sequence + 1;
411 for (l = self->files; l; l = l->next) {
413 f1->sequence = mediasequence;
417 /* Match, check that all following ones are matching too and continue
418 * sequence numbers from there on */
420 mediasequence = f2->sequence;
422 for (; l; l = l->next) {
424 f2 = m ? m->data : NULL;
426 f1->sequence = mediasequence;
430 if (!g_str_equal (f1->uri, f2->uri)) {
431 GST_WARNING ("Inconsistent URIs after playlist update");
445 * @data: a m3u8 playlist text data, taking ownership
448 gst_m3u8_update (GstM3U8 * self, gchar * data)
451 GstClockTime duration;
453 gboolean discontinuity = FALSE;
454 gchar *current_key = NULL;
455 gboolean have_iv = FALSE;
456 guint8 iv[16] = { 0, };
457 gint64 size = -1, offset = -1;
458 gint64 mediasequence;
459 GList *previous_files = NULL;
460 gboolean have_mediasequence = FALSE;
462 g_return_val_if_fail (self != NULL, FALSE);
463 g_return_val_if_fail (data != NULL, FALSE);
465 GST_M3U8_LOCK (self);
467 /* check if the data changed since last update */
468 if (self->last_data && g_str_equal (self->last_data, data)) {
469 GST_DEBUG ("Playlist is the same as previous one");
471 GST_M3U8_UNLOCK (self);
475 if (!g_str_has_prefix (data, "#EXTM3U")) {
476 GST_WARNING ("Data doesn't start with #EXTM3U");
478 GST_M3U8_UNLOCK (self);
482 if (g_strrstr (data, "\n#EXT-X-STREAM-INF:") != NULL) {
483 GST_WARNING ("Not a media playlist, but a master playlist!");
484 GST_M3U8_UNLOCK (self);
488 GST_TRACE ("data:\n%s", data);
490 g_free (self->last_data);
491 self->last_data = data;
493 self->current_file = NULL;
494 previous_files = self->files;
496 self->duration = GST_CLOCK_TIME_NONE;
499 /* By default, allow caching */
500 self->allowcache = TRUE;
508 end = g_utf8_strchr (data, -1, '\n');
512 r = g_utf8_strchr (data, -1, '\r');
516 if (data[0] != '#' && data[0] != '\0') {
518 GST_LOG ("%s: got line without EXTINF, dropping", data);
522 data = uri_join (self->base_uri ? self->base_uri : self->uri, data);
524 GstM3U8MediaFile *file;
525 file = gst_m3u8_media_file_new (data, title, duration, mediasequence++);
527 /* set encryption params */
528 file->key = current_key ? g_strdup (current_key) : NULL;
531 memcpy (file->iv, iv, sizeof (iv));
533 guint8 *iv = file->iv + 12;
534 GST_WRITE_UINT32_BE (iv, file->sequence);
541 file->offset = offset;
543 GstM3U8MediaFile *prev = self->files ? self->files->data : NULL;
548 offset = prev->offset + prev->size;
550 file->offset = offset;
557 file->discont = discontinuity;
561 discontinuity = FALSE;
563 self->files = g_list_prepend (self->files, file);
566 } else if (g_str_has_prefix (data, "#EXTINF:")) {
568 if (!double_from_string (data + 8, &data, &fval)) {
569 GST_WARNING ("Can't read EXTINF duration");
572 duration = fval * (gdouble) GST_SECOND;
573 if (self->targetduration > 0 && duration > self->targetduration) {
574 GST_WARNING ("EXTINF duration (%" GST_TIME_FORMAT
575 ") > TARGETDURATION (%" GST_TIME_FORMAT ")",
576 GST_TIME_ARGS (duration), GST_TIME_ARGS (self->targetduration));
578 if (!data || *data != ',')
580 data = g_utf8_next_char (data);
583 title = g_strdup (data);
585 } else if (g_str_has_prefix (data, "#EXT-X-")) {
586 gchar *data_ext_x = data + 7;
588 /* All these entries start with #EXT-X- */
589 if (g_str_has_prefix (data_ext_x, "ENDLIST")) {
590 self->endlist = TRUE;
591 } else if (g_str_has_prefix (data_ext_x, "VERSION:")) {
592 if (int_from_string (data + 15, &data, &val))
594 } else if (g_str_has_prefix (data_ext_x, "TARGETDURATION:")) {
595 if (int_from_string (data + 22, &data, &val))
596 self->targetduration = val * GST_SECOND;
597 } else if (g_str_has_prefix (data_ext_x, "MEDIA-SEQUENCE:")) {
598 if (int_from_string (data + 22, &data, &val)) {
600 have_mediasequence = TRUE;
602 } else if (g_str_has_prefix (data_ext_x, "DISCONTINUITY-SEQUENCE:")) {
603 if (int_from_string (data + 30, &data, &val)
604 && val != self->discont_sequence) {
605 self->discont_sequence = val;
606 discontinuity = TRUE;
608 } else if (g_str_has_prefix (data_ext_x, "DISCONTINUITY")) {
609 self->discont_sequence++;
610 discontinuity = TRUE;
611 } else if (g_str_has_prefix (data_ext_x, "PROGRAM-DATE-TIME:")) {
612 /* <YYYY-MM-DDThh:mm:ssZ> */
613 GST_DEBUG ("FIXME parse date");
614 } else if (g_str_has_prefix (data_ext_x, "ALLOW-CACHE:")) {
615 self->allowcache = g_ascii_strcasecmp (data + 19, "YES") == 0;
616 } else if (g_str_has_prefix (data_ext_x, "KEY:")) {
621 /* IV and KEY are only valid until the next #EXT-X-KEY */
623 g_free (current_key);
625 while (data && parse_attributes (&data, &a, &v)) {
626 if (g_str_equal (a, "URI")) {
628 uri_join (self->base_uri ? self->base_uri : self->uri, v);
629 } else if (g_str_equal (a, "IV")) {
633 if (strlen (ivp) < 32 + 2 || (!g_str_has_prefix (ivp, "0x")
634 && !g_str_has_prefix (ivp, "0X"))) {
635 GST_WARNING ("Can't read IV");
640 for (i = 0; i < 16; i++) {
643 h = g_ascii_xdigit_value (*ivp);
645 l = g_ascii_xdigit_value (*ivp);
647 if (h == -1 || l == -1) {
651 iv[i] = (h << 4) | l;
655 GST_WARNING ("Can't read IV");
659 } else if (g_str_equal (a, "METHOD")) {
660 if (!g_str_equal (v, "AES-128")) {
661 GST_WARNING ("Encryption method %s not supported", v);
666 } else if (g_str_has_prefix (data_ext_x, "BYTERANGE:")) {
667 gchar *v = data + 17;
669 if (int64_from_string (v, &v, &size)) {
670 if (*v == '@' && !int64_from_string (v + 1, &v, &offset))
676 GST_LOG ("Ignored line: %s", data);
679 GST_LOG ("Ignored line: %s", data);
685 data = g_utf8_next_char (end); /* skip \n */
688 g_free (current_key);
691 self->files = g_list_reverse (self->files);
693 if (previous_files) {
694 gboolean consistent = gst_m3u8_update_check_consistent_media_seqnums (self,
695 have_mediasequence, previous_files);
697 g_list_foreach (previous_files, (GFunc) gst_m3u8_media_file_unref, NULL);
698 g_list_free (previous_files);
699 previous_files = NULL;
701 /* error was reported above already */
703 GST_M3U8_UNLOCK (self);
708 if (self->files == NULL) {
709 GST_ERROR ("Invalid media playlist, it does not contain any media files");
710 GST_M3U8_UNLOCK (self);
714 /* calculate the start and end times of this media playlist. */
717 GstM3U8MediaFile *file;
718 GstClockTime duration = 0;
722 for (walk = self->files; walk; walk = walk->next) {
725 if (mediasequence == -1) {
726 mediasequence = file->sequence;
727 } else if (mediasequence >= file->sequence) {
728 GST_ERROR ("Non-increasing media sequence");
729 GST_M3U8_UNLOCK (self);
732 mediasequence = file->sequence;
735 duration += file->duration;
736 if (file->sequence > self->highest_sequence_number) {
737 if (self->highest_sequence_number >= 0) {
738 /* if an update of the media playlist has been missed, there
739 will be a gap between self->highest_sequence_number and the
740 first sequence number in this media playlist. In this situation
741 assume that the missing fragments had a duration of
742 targetduration each */
743 self->last_file_end +=
744 (file->sequence - self->highest_sequence_number -
745 1) * self->targetduration;
747 self->last_file_end += file->duration;
748 self->highest_sequence_number = file->sequence;
751 if (GST_M3U8_IS_LIVE (self)) {
752 self->first_file_start = self->last_file_end - duration;
753 GST_DEBUG ("Live playlist range %" GST_TIME_FORMAT " -> %"
754 GST_TIME_FORMAT, GST_TIME_ARGS (self->first_file_start),
755 GST_TIME_ARGS (self->last_file_end));
757 self->duration = duration;
760 /* first-time setup */
761 if (self->files && self->sequence == -1) {
764 if (GST_M3U8_IS_LIVE (self)) {
766 GstClockTime sequence_pos = 0;
768 file = g_list_last (self->files);
770 if (self->last_file_end >= GST_M3U8_MEDIA_FILE (file->data)->duration) {
772 self->last_file_end - GST_M3U8_MEDIA_FILE (file->data)->duration;
775 /* for live streams, start GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE from
776 * the end of the playlist. See section 6.3.3 of HLS draft */
777 for (i = 0; i < GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE && file->prev &&
778 GST_M3U8_MEDIA_FILE (file->prev->data)->duration <= sequence_pos;
781 sequence_pos -= GST_M3U8_MEDIA_FILE (file->data)->duration;
783 self->sequence_position = sequence_pos;
785 file = g_list_first (self->files);
786 self->sequence_position = 0;
788 self->current_file = file;
789 self->sequence = GST_M3U8_MEDIA_FILE (file->data)->sequence;
790 GST_DEBUG ("first sequence: %u", (guint) self->sequence);
793 GST_LOG ("processed media playlist %s, %u fragments", self->name,
794 g_list_length (self->files));
796 GST_M3U8_UNLOCK (self);
801 /* call with M3U8_LOCK held */
803 m3u8_find_next_fragment (GstM3U8 * m3u8, gboolean forward)
805 GstM3U8MediaFile *file;
806 GList *l = m3u8->files;
812 if (file->sequence >= m3u8->sequence)
823 if (file->sequence <= m3u8->sequence)
834 gst_m3u8_get_next_fragment (GstM3U8 * m3u8, gboolean forward,
835 GstClockTime * sequence_position, gboolean * discont)
837 GstM3U8MediaFile *file = NULL;
839 g_return_val_if_fail (m3u8 != NULL, NULL);
841 GST_M3U8_LOCK (m3u8);
843 GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, m3u8->sequence);
845 if (m3u8->sequence < 0) /* can't happen really */
848 if (m3u8->current_file == NULL)
849 m3u8->current_file = m3u8_find_next_fragment (m3u8, forward);
851 if (m3u8->current_file == NULL)
854 file = gst_m3u8_media_file_ref (m3u8->current_file->data);
856 GST_DEBUG ("Got fragment with sequence %u (current sequence %u)",
857 (guint) file->sequence, (guint) m3u8->sequence);
859 if (sequence_position)
860 *sequence_position = m3u8->sequence_position;
862 *discont = file->discont || (m3u8->sequence != file->sequence);
864 m3u8->current_file_duration = file->duration;
865 m3u8->sequence = file->sequence;
869 GST_M3U8_UNLOCK (m3u8);
875 gst_m3u8_has_next_fragment (GstM3U8 * m3u8, gboolean forward)
880 g_return_val_if_fail (m3u8 != NULL, FALSE);
882 GST_M3U8_LOCK (m3u8);
884 GST_DEBUG ("Checking next fragment %" G_GINT64_FORMAT,
885 m3u8->sequence + (forward ? 1 : -1));
887 if (m3u8->current_file) {
888 cur = m3u8->current_file;
890 cur = m3u8_find_next_fragment (m3u8, forward);
893 have_next = cur && ((forward && cur->next) || (!forward && cur->prev));
895 GST_M3U8_UNLOCK (m3u8);
900 /* call with M3U8_LOCK held */
902 m3u8_alternate_advance (GstM3U8 * m3u8, gboolean forward)
904 gint targetnum = m3u8->sequence;
906 GstM3U8MediaFile *mf;
908 /* figure out the target seqnum */
914 for (tmp = m3u8->files; tmp; tmp = tmp->next) {
915 mf = (GstM3U8MediaFile *) tmp->data;
916 if (mf->sequence == targetnum)
920 GST_WARNING ("Can't find next fragment");
923 m3u8->current_file = tmp;
924 m3u8->sequence = targetnum;
925 m3u8->current_file_duration = GST_M3U8_MEDIA_FILE (tmp->data)->duration;
929 gst_m3u8_advance_fragment (GstM3U8 * m3u8, gboolean forward)
931 GstM3U8MediaFile *file;
933 g_return_if_fail (m3u8 != NULL);
935 GST_M3U8_LOCK (m3u8);
937 GST_DEBUG ("Sequence position was %" GST_TIME_FORMAT,
938 GST_TIME_ARGS (m3u8->sequence_position));
939 if (GST_CLOCK_TIME_IS_VALID (m3u8->current_file_duration)) {
940 /* Advance our position based on the previous fragment we played */
942 m3u8->sequence_position += m3u8->current_file_duration;
943 else if (m3u8->current_file_duration < m3u8->sequence_position)
944 m3u8->sequence_position -= m3u8->current_file_duration;
946 m3u8->sequence_position = 0;
947 m3u8->current_file_duration = GST_CLOCK_TIME_NONE;
948 GST_DEBUG ("Sequence position now %" GST_TIME_FORMAT,
949 GST_TIME_ARGS (m3u8->sequence_position));
951 if (!m3u8->current_file) {
954 GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, m3u8->sequence);
955 for (l = m3u8->files; l != NULL; l = l->next) {
956 if (GST_M3U8_MEDIA_FILE (l->data)->sequence == m3u8->sequence) {
957 m3u8->current_file = l;
961 if (m3u8->current_file == NULL) {
963 ("Could not find current fragment, trying next fragment directly");
964 m3u8_alternate_advance (m3u8, forward);
966 /* Resync sequence number if the above has failed for live streams */
967 if (m3u8->current_file == NULL && GST_M3U8_IS_LIVE (m3u8)) {
968 /* for live streams, start GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE from
969 the end of the playlist. See section 6.3.3 of HLS draft */
971 g_list_length (m3u8->files) - GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE;
972 m3u8->current_file = g_list_nth (m3u8->files, pos >= 0 ? pos : 0);
973 m3u8->current_file_duration =
974 GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->duration;
976 GST_WARNING ("Resyncing live playlist");
982 file = GST_M3U8_MEDIA_FILE (m3u8->current_file->data);
983 GST_DEBUG ("Advancing from sequence %u", (guint) file->sequence);
985 m3u8->current_file = m3u8->current_file->next;
986 if (m3u8->current_file) {
987 m3u8->sequence = GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->sequence;
989 m3u8->sequence = file->sequence + 1;
992 m3u8->current_file = m3u8->current_file->prev;
993 if (m3u8->current_file) {
994 m3u8->sequence = GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->sequence;
996 m3u8->sequence = file->sequence - 1;
999 if (m3u8->current_file) {
1000 /* Store duration of the fragment we're using to update the position
1001 * the next time we advance */
1002 m3u8->current_file_duration =
1003 GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->duration;
1008 GST_M3U8_UNLOCK (m3u8);
1012 gst_m3u8_get_duration (GstM3U8 * m3u8)
1014 GstClockTime duration = GST_CLOCK_TIME_NONE;
1016 g_return_val_if_fail (m3u8 != NULL, GST_CLOCK_TIME_NONE);
1018 GST_M3U8_LOCK (m3u8);
1020 /* We can only get the duration for on-demand streams */
1024 if (!GST_CLOCK_TIME_IS_VALID (m3u8->duration) && m3u8->files != NULL) {
1028 for (f = m3u8->files; f != NULL; f = f->next)
1029 m3u8->duration += GST_M3U8_MEDIA_FILE (f)->duration;
1031 duration = m3u8->duration;
1035 GST_M3U8_UNLOCK (m3u8);
1041 gst_m3u8_get_target_duration (GstM3U8 * m3u8)
1043 GstClockTime target_duration;
1045 g_return_val_if_fail (m3u8 != NULL, GST_CLOCK_TIME_NONE);
1047 GST_M3U8_LOCK (m3u8);
1048 target_duration = m3u8->targetduration;
1049 GST_M3U8_UNLOCK (m3u8);
1051 return target_duration;
1055 gst_m3u8_get_uri (GstM3U8 * m3u8)
1059 GST_M3U8_LOCK (m3u8);
1060 uri = g_strdup (m3u8->uri);
1061 GST_M3U8_UNLOCK (m3u8);
1067 gst_m3u8_is_live (GstM3U8 * m3u8)
1071 g_return_val_if_fail (m3u8 != NULL, FALSE);
1073 GST_M3U8_LOCK (m3u8);
1074 is_live = GST_M3U8_IS_LIVE (m3u8);
1075 GST_M3U8_UNLOCK (m3u8);
1081 uri_join (const gchar * uri1, const gchar * uri2)
1083 gchar *uri_copy, *tmp, *ret = NULL;
1085 if (gst_uri_is_valid (uri2))
1086 return g_strdup (uri2);
1088 uri_copy = g_strdup (uri1);
1089 if (uri2[0] != '/') {
1090 /* uri2 is a relative uri2 */
1091 /* look for query params */
1092 tmp = g_utf8_strchr (uri_copy, -1, '?');
1094 /* find last / char, ignoring query params */
1095 tmp = g_utf8_strrchr (uri_copy, tmp - uri_copy, '/');
1097 /* find last / char in URL */
1098 tmp = g_utf8_strrchr (uri_copy, -1, '/');
1101 GST_WARNING ("Can't build a valid uri_copy");
1106 ret = g_strdup_printf ("%s/%s", uri_copy, uri2);
1108 /* uri2 is an absolute uri2 */
1109 char *scheme, *hostname;
1112 /* find the : in <scheme>:// */
1113 tmp = g_utf8_strchr (uri_copy, -1, ':');
1115 GST_WARNING ("Can't build a valid uri_copy");
1124 tmp = g_utf8_strchr (hostname, -1, '/');
1128 ret = g_strdup_printf ("%s://%s%s", scheme, hostname, uri2);
1137 gst_m3u8_get_seek_range (GstM3U8 * m3u8, gint64 * start, gint64 * stop)
1139 GstClockTime duration = 0;
1141 GstM3U8MediaFile *file;
1143 guint min_distance = 0;
1145 g_return_val_if_fail (m3u8 != NULL, FALSE);
1147 GST_M3U8_LOCK (m3u8);
1149 if (m3u8->files == NULL)
1152 if (GST_M3U8_IS_LIVE (m3u8)) {
1153 /* min_distance is used to make sure the seek range is never closer than
1154 GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE fragments from the end of a live
1155 playlist - see 6.3.3. "Playing the Playlist file" of the HLS draft */
1156 min_distance = GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE;
1158 count = g_list_length (m3u8->files);
1160 for (walk = m3u8->files; walk && count > min_distance; walk = walk->next) {
1163 duration += file->duration;
1169 *start = m3u8->first_file_start;
1170 *stop = *start + duration;
1174 GST_M3U8_UNLOCK (m3u8);
1175 return (duration > 0);
1179 gst_hls_media_ref (GstHLSMedia * media)
1181 g_assert (media != NULL && media->ref_count > 0);
1182 g_atomic_int_add (&media->ref_count, 1);
1187 gst_hls_media_unref (GstHLSMedia * media)
1189 g_assert (media != NULL && media->ref_count > 0);
1190 if (g_atomic_int_dec_and_test (&media->ref_count)) {
1191 if (media->playlist)
1192 gst_m3u8_unref (media->playlist);
1193 g_free (media->group_id);
1194 g_free (media->name);
1195 g_free (media->uri);
1196 g_free (media->lang);
1201 static GstHLSMediaType
1202 gst_m3u8_get_hls_media_type_from_string (const gchar * type_name)
1204 if (strcmp (type_name, "AUDIO") == 0)
1205 return GST_HLS_MEDIA_TYPE_AUDIO;
1206 if (strcmp (type_name, "VIDEO") == 0)
1207 return GST_HLS_MEDIA_TYPE_VIDEO;
1208 if (strcmp (type_name, "SUBTITLES") == 0)
1209 return GST_HLS_MEDIA_TYPE_SUBTITLES;
1210 if (strcmp (type_name, "CLOSED_CAPTIONS") == 0)
1211 return GST_HLS_MEDIA_TYPE_CLOSED_CAPTIONS;
1213 return GST_HLS_MEDIA_TYPE_INVALID;
1216 #define GST_HLS_MEDIA_TYPE_NAME(mtype) gst_m3u8_hls_media_type_get_nick(mtype)
1217 static inline const gchar *
1218 gst_m3u8_hls_media_type_get_nick (GstHLSMediaType mtype)
1220 static const gchar *nicks[GST_HLS_N_MEDIA_TYPES] = { "audio", "video",
1221 "subtitle", "closed-captions"
1224 if (mtype < 0 || mtype >= GST_HLS_N_MEDIA_TYPES)
1227 return nicks[mtype];
1230 /* returns unquoted copy of string */
1232 gst_m3u8_unquote (const gchar * str)
1234 const gchar *start, *end;
1236 start = strchr (str, '"');
1238 return g_strdup (str);
1239 end = strchr (start + 1, '"');
1241 GST_WARNING ("Broken quoted string [%s] - can't find end quote", str);
1242 return g_strdup (start + 1);
1244 return g_strndup (start + 1, (gsize) (end - (start + 1)));
1247 static GstHLSMedia *
1248 gst_m3u8_parse_media (gchar * desc, const gchar * base_uri)
1253 media = g_new0 (GstHLSMedia, 1);
1254 media->ref_count = 1;
1255 media->playlist = gst_m3u8_new ();
1256 media->mtype = GST_HLS_MEDIA_TYPE_INVALID;
1258 GST_LOG ("parsing %s", desc);
1259 while (desc != NULL && parse_attributes (&desc, &a, &v)) {
1260 if (strcmp (a, "TYPE") == 0) {
1261 media->mtype = gst_m3u8_get_hls_media_type_from_string (v);
1262 } else if (strcmp (a, "GROUP-ID") == 0) {
1263 g_free (media->group_id);
1264 media->group_id = gst_m3u8_unquote (v);
1265 } else if (strcmp (a, "NAME") == 0) {
1266 g_free (media->name);
1267 media->name = gst_m3u8_unquote (v);
1268 } else if (strcmp (a, "URI") == 0) {
1271 g_free (media->uri);
1272 uri = gst_m3u8_unquote (v);
1273 media->uri = uri_join (base_uri, uri);
1275 } else if (strcmp (a, "LANGUAGE") == 0) {
1276 g_free (media->lang);
1277 media->lang = gst_m3u8_unquote (v);
1278 } else if (strcmp (a, "DEFAULT") == 0) {
1279 media->is_default = g_ascii_strcasecmp (v, "yes") == 0;
1280 } else if (strcmp (a, "FORCED") == 0) {
1281 media->forced = g_ascii_strcasecmp (v, "yes") == 0;
1282 } else if (strcmp (a, "AUTOSELECT") == 0) {
1283 media->autoselect = g_ascii_strcasecmp (v, "yes") == 0;
1285 /* unhandled: ASSOC-LANGUAGE, INSTREAM-ID, CHARACTERISTICS */
1286 GST_FIXME ("EXT-X-MEDIA: unhandled attribute: %s = %s", a, v);
1290 if (media->mtype == GST_HLS_MEDIA_TYPE_INVALID)
1291 goto required_attributes_missing;
1293 if (media->uri == NULL)
1294 goto existing_stream;
1296 if (media->group_id == NULL || media->name == NULL)
1297 goto required_attributes_missing;
1299 if (media->mtype == GST_HLS_MEDIA_TYPE_CLOSED_CAPTIONS)
1302 GST_DEBUG ("media: %s, group '%s', name '%s', uri '%s', %s %s %s, lang=%s",
1303 GST_HLS_MEDIA_TYPE_NAME (media->mtype), media->group_id, media->name,
1304 media->uri, media->is_default ? "default" : "-",
1305 media->autoselect ? "autoselect" : "-",
1306 media->forced ? "forced" : "-", media->lang ? media->lang : "??");
1312 GST_WARNING ("closed captions EXT-X-MEDIA should not have URI specified");
1315 required_attributes_missing:
1317 GST_WARNING ("EXT-X-MEDIA description is missing required attributes");
1323 GST_DEBUG ("EXT-X-MEDIA without URI, describes embedded stream, skipping");
1329 gst_hls_media_unref (media);
1334 static GstHLSVariantStream *
1335 gst_hls_variant_stream_new (void)
1337 GstHLSVariantStream *stream;
1339 stream = g_new0 (GstHLSVariantStream, 1);
1340 stream->m3u8 = gst_m3u8_new ();
1341 stream->refcount = 1;
1345 GstHLSVariantStream *
1346 gst_hls_variant_stream_ref (GstHLSVariantStream * stream)
1348 g_atomic_int_inc (&stream->refcount);
1353 gst_hls_variant_stream_unref (GstHLSVariantStream * stream)
1355 if (g_atomic_int_dec_and_test (&stream->refcount)) {
1358 g_free (stream->name);
1359 g_free (stream->uri);
1360 g_free (stream->codecs);
1361 gst_m3u8_unref (stream->m3u8);
1362 for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
1363 g_free (stream->media_groups[i]);
1364 g_list_free_full (stream->media[i], (GDestroyNotify) gst_hls_media_unref);
1370 static GstHLSVariantStream *
1371 find_variant_stream_by_name (GList * list, const gchar * name)
1373 for (; list != NULL; list = list->next) {
1374 GstHLSVariantStream *variant_stream = list->data;
1376 if (variant_stream->name != NULL && !strcmp (variant_stream->name, name))
1377 return variant_stream;
1382 static GstHLSVariantStream *
1383 find_variant_stream_by_uri (GList * list, const gchar * uri)
1385 for (; list != NULL; list = list->next) {
1386 GstHLSVariantStream *variant_stream = list->data;
1388 if (variant_stream->uri != NULL && !strcmp (variant_stream->uri, uri))
1389 return variant_stream;
1394 static GstHLSMasterPlaylist *
1395 gst_hls_master_playlist_new (void)
1397 GstHLSMasterPlaylist *playlist;
1399 playlist = g_new0 (GstHLSMasterPlaylist, 1);
1400 playlist->refcount = 1;
1401 playlist->is_simple = FALSE;
1407 gst_hls_master_playlist_unref (GstHLSMasterPlaylist * playlist)
1409 if (g_atomic_int_dec_and_test (&playlist->refcount)) {
1410 g_list_free_full (playlist->variants,
1411 (GDestroyNotify) gst_hls_variant_stream_unref);
1412 g_list_free_full (playlist->iframe_variants,
1413 (GDestroyNotify) gst_hls_variant_stream_unref);
1414 if (playlist->default_variant)
1415 gst_hls_variant_stream_unref (playlist->default_variant);
1416 g_free (playlist->last_data);
1422 hls_media_name_compare_func (gconstpointer media, gconstpointer name)
1424 return strcmp (((GstHLSMedia *) media)->name, (const gchar *) name);
1427 /* Takes ownership of @data */
1428 GstHLSMasterPlaylist *
1429 gst_hls_master_playlist_new_from_data (gchar * data, const gchar * base_uri)
1431 GHashTable *media_groups[GST_HLS_N_MEDIA_TYPES] = { NULL, };
1432 GstHLSMasterPlaylist *playlist;
1433 GstHLSVariantStream *pending_stream;
1434 gchar *end, *free_data = data;
1438 if (!g_str_has_prefix (data, "#EXTM3U")) {
1439 GST_WARNING ("Data doesn't start with #EXTM3U");
1444 playlist = gst_hls_master_playlist_new ();
1446 /* store data before we modify it for parsing */
1447 playlist->last_data = g_strdup (data);
1449 GST_TRACE ("data:\n%s", data);
1451 if (strstr (data, "\n#EXTINF:") != NULL) {
1452 GST_INFO ("This is a simple media playlist, not a master playlist");
1454 pending_stream = gst_hls_variant_stream_new ();
1455 pending_stream->name = g_strdup (base_uri);
1456 pending_stream->uri = g_strdup (base_uri);
1457 gst_m3u8_set_uri (pending_stream->m3u8, base_uri, NULL, base_uri);
1458 playlist->variants = g_list_append (playlist->variants, pending_stream);
1459 playlist->default_variant = gst_hls_variant_stream_ref (pending_stream);
1460 playlist->is_simple = TRUE;
1462 if (!gst_m3u8_update (pending_stream->m3u8, data)) {
1463 GST_WARNING ("Failed to parse media playlist");
1464 gst_hls_master_playlist_unref (playlist);
1470 pending_stream = NULL;
1475 end = g_utf8_strchr (data, -1, '\n');
1479 r = g_utf8_strchr (data, -1, '\r');
1483 if (data[0] != '#' && data[0] != '\0') {
1486 if (pending_stream == NULL) {
1487 GST_LOG ("%s: got line without EXT-STREAM-INF, dropping", data);
1492 uri = uri_join (base_uri, name);
1496 pending_stream->name = g_strdup (name);
1497 pending_stream->uri = uri;
1499 if (find_variant_stream_by_name (playlist->variants, name)
1500 || find_variant_stream_by_uri (playlist->variants, uri)) {
1501 GST_DEBUG ("Already have a list with this name or URI: %s", name);
1502 gst_hls_variant_stream_unref (pending_stream);
1504 GST_INFO ("stream %s @ %u: %s", name, pending_stream->bandwidth, uri);
1505 gst_m3u8_set_uri (pending_stream->m3u8, uri, NULL, name);
1506 playlist->variants = g_list_append (playlist->variants, pending_stream);
1507 /* use first stream in the playlist as default */
1508 if (playlist->default_variant == NULL) {
1509 playlist->default_variant =
1510 gst_hls_variant_stream_ref (pending_stream);
1513 pending_stream = NULL;
1514 } else if (g_str_has_prefix (data, "#EXT-X-VERSION:")) {
1515 if (int_from_string (data + 15, &data, &val))
1516 playlist->version = val;
1517 } else if (g_str_has_prefix (data, "#EXT-X-STREAM-INF:") ||
1518 g_str_has_prefix (data, "#EXT-X-I-FRAME-STREAM-INF:")) {
1519 GstHLSVariantStream *stream;
1522 stream = gst_hls_variant_stream_new ();
1523 stream->iframe = g_str_has_prefix (data, "#EXT-X-I-FRAME-STREAM-INF:");
1524 data += stream->iframe ? 26 : 18;
1525 while (data && parse_attributes (&data, &a, &v)) {
1526 if (g_str_equal (a, "BANDWIDTH")) {
1527 if (!int_from_string (v, NULL, &stream->bandwidth))
1528 GST_WARNING ("Error while reading BANDWIDTH");
1529 } else if (g_str_equal (a, "PROGRAM-ID")) {
1530 if (!int_from_string (v, NULL, &stream->program_id))
1531 GST_WARNING ("Error while reading PROGRAM-ID");
1532 } else if (g_str_equal (a, "CODECS")) {
1533 g_free (stream->codecs);
1534 stream->codecs = g_strdup (v);
1535 } else if (g_str_equal (a, "RESOLUTION")) {
1536 if (!int_from_string (v, &v, &stream->width))
1537 GST_WARNING ("Error while reading RESOLUTION width");
1538 if (!v || *v != 'x') {
1539 GST_WARNING ("Missing height");
1541 v = g_utf8_next_char (v);
1542 if (!int_from_string (v, NULL, &stream->height))
1543 GST_WARNING ("Error while reading RESOLUTION height");
1545 } else if (stream->iframe && g_str_equal (a, "URI")) {
1546 stream->uri = uri_join (base_uri, v);
1547 if (stream->uri != NULL) {
1548 stream->name = g_strdup (stream->uri);
1549 gst_m3u8_set_uri (stream->m3u8, stream->uri, NULL, stream->name);
1551 gst_hls_variant_stream_unref (stream);
1553 } else if (g_str_equal (a, "AUDIO")) {
1554 g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_AUDIO]);
1555 stream->media_groups[GST_HLS_MEDIA_TYPE_AUDIO] = gst_m3u8_unquote (v);
1556 } else if (g_str_equal (a, "SUBTITLES")) {
1557 g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_SUBTITLES]);
1558 stream->media_groups[GST_HLS_MEDIA_TYPE_SUBTITLES] =
1559 gst_m3u8_unquote (v);
1560 } else if (g_str_equal (a, "VIDEO")) {
1561 g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_VIDEO]);
1562 stream->media_groups[GST_HLS_MEDIA_TYPE_VIDEO] = gst_m3u8_unquote (v);
1563 } else if (g_str_equal (a, "CLOSED-CAPTIONS")) {
1564 /* closed captions will be embedded inside the video stream, ignore */
1568 if (stream->iframe) {
1569 if (find_variant_stream_by_uri (playlist->iframe_variants, stream->uri)) {
1570 GST_DEBUG ("Already have a list with this URI");
1571 gst_hls_variant_stream_unref (stream);
1573 playlist->iframe_variants =
1574 g_list_append (playlist->iframe_variants, stream);
1577 if (pending_stream != NULL) {
1578 GST_WARNING ("variant stream without uri, dropping");
1579 gst_hls_variant_stream_unref (pending_stream);
1581 pending_stream = stream;
1583 } else if (g_str_has_prefix (data, "#EXT-X-MEDIA:")) {
1587 media = gst_m3u8_parse_media (data + strlen ("#EXT-X-MEDIA:"), base_uri);
1592 if (media_groups[media->mtype] == NULL) {
1593 media_groups[media->mtype] =
1594 g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
1597 list = g_hash_table_lookup (media_groups[media->mtype], media->group_id);
1599 /* make sure there isn't already a media with the same name */
1600 if (!g_list_find_custom (list, media->name, hls_media_name_compare_func)) {
1601 g_hash_table_replace (media_groups[media->mtype],
1602 g_strdup (media->group_id), g_list_append (list, media));
1603 GST_INFO ("Added media %s to group %s", media->name, media->group_id);
1605 GST_WARNING (" media with name '%s' already exists in group '%s'!",
1606 media->name, media->group_id);
1607 gst_hls_media_unref (media);
1609 } else if (*data != '\0') {
1610 GST_LOG ("Ignored line: %s", data);
1616 data = g_utf8_next_char (end); /* skip \n */
1619 if (pending_stream != NULL) {
1620 GST_WARNING ("#EXT-X-STREAM-INF without uri, dropping");
1621 gst_hls_variant_stream_unref (pending_stream);
1626 /* Add alternative renditions media to variant streams */
1627 for (l = playlist->variants; l != NULL; l = l->next) {
1628 GstHLSVariantStream *stream = l->data;
1631 for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
1632 if (stream->media_groups[i] != NULL && media_groups[i] != NULL) {
1633 GST_INFO ("Adding %s group '%s' to stream '%s'",
1634 GST_HLS_MEDIA_TYPE_NAME (i), stream->media_groups[i], stream->name);
1636 mlist = g_hash_table_lookup (media_groups[i], stream->media_groups[i]);
1639 GST_WARNING ("Group '%s' does not exist!", stream->media_groups[i]);
1641 while (mlist != NULL) {
1642 GstHLSMedia *media = mlist->data;
1644 GST_DEBUG (" %s media %s, uri: %s", GST_HLS_MEDIA_TYPE_NAME (i),
1645 media->name, media->uri);
1648 g_list_append (stream->media[i], gst_hls_media_ref (media));
1649 mlist = mlist->next;
1655 /* clean up our temporary alternative rendition groups hash tables */
1656 for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
1657 if (media_groups[i] != NULL) {
1658 GList *groups, *mlist;
1660 groups = g_hash_table_get_keys (media_groups[i]);
1661 for (l = groups; l != NULL; l = l->next) {
1662 mlist = g_hash_table_lookup (media_groups[i], l->data);
1663 g_list_free_full (mlist, (GDestroyNotify) gst_hls_media_unref);
1665 g_list_free (groups);
1666 g_hash_table_unref (media_groups[i]);
1670 if (playlist->variants == NULL) {
1671 GST_WARNING ("Master playlist without any media playlists!");
1672 gst_hls_master_playlist_unref (playlist);
1676 /* reorder variants by bitrate */
1677 playlist->variants =
1678 g_list_sort (playlist->variants,
1679 (GCompareFunc) gst_hls_variant_stream_compare_by_bitrate);
1681 playlist->iframe_variants =
1682 g_list_sort (playlist->iframe_variants,
1683 (GCompareFunc) gst_hls_variant_stream_compare_by_bitrate);
1685 /* FIXME: restore old current_variant after master playlist update
1686 * (move into code that does that update) */
1689 gchar *top_variant_uri = NULL;
1690 gboolean iframe = FALSE;
1692 if (!self->current_variant) {
1693 top_variant_uri = GST_M3U8 (self->lists->data)->uri;
1695 top_variant_uri = GST_M3U8 (self->current_variant->data)->uri;
1696 iframe = GST_M3U8 (self->current_variant->data)->iframe;
1699 /* here we sorted the lists */
1702 playlist->current_variant =
1703 find_variant_stream_by_uri (playlist->iframe_variants,
1706 playlist->current_variant =
1707 find_variant_stream_by_uri (playlist->variants, top_variant_uri);
1711 GST_DEBUG ("parsed master playlist with %d streams and %d I-frame streams",
1712 g_list_length (playlist->variants),
1713 g_list_length (playlist->iframe_variants));
1720 gst_hls_variant_stream_is_live (GstHLSVariantStream * variant)
1724 g_return_val_if_fail (variant != NULL, FALSE);
1726 is_live = gst_m3u8_is_live (variant->m3u8);
1732 compare_media (const GstHLSMedia * a, const GstHLSMedia * b)
1734 return strcmp (a->name, b->name);
1738 gst_hls_variant_find_matching_media (GstHLSVariantStream * stream,
1739 GstHLSMedia * media)
1741 GList *mlist = stream->media[media->mtype];
1747 match = g_list_find_custom (mlist, media, (GCompareFunc) compare_media);
1754 GstHLSVariantStream *
1755 gst_hls_master_playlist_get_variant_for_bitrate (GstHLSMasterPlaylist *
1756 playlist, GstHLSVariantStream * current_variant, guint bitrate)
1758 GstHLSVariantStream *variant = current_variant;
1761 /* variant lists are sorted low to high, so iterate from highest to lowest */
1762 if (current_variant == NULL || !current_variant->iframe)
1763 l = g_list_last (playlist->variants);
1765 l = g_list_last (playlist->iframe_variants);
1769 if (variant->bandwidth <= bitrate)
1777 GstHLSVariantStream *
1778 gst_hls_master_playlist_get_matching_variant (GstHLSMasterPlaylist * playlist,
1779 GstHLSVariantStream * current_variant)
1781 if (current_variant->iframe) {
1782 return find_variant_stream_by_uri (playlist->iframe_variants,
1783 current_variant->uri);
1786 return find_variant_stream_by_uri (playlist->variants, current_variant->uri);