1 /* GStreamer
2  * Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
3  * Copyright (C) 2015 Tim-Philipp Müller <tim@centricular.com>
4  *
5  * m3u8.c:
6  *
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.
11  *
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.
16  *
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.
21  */
22 
23 #include <stdlib.h>
24 #include <math.h>
25 #include <errno.h>
26 #include <glib.h>
27 #include <string.h>
28 
29 #include "gsthls.h"
30 #include "m3u8.h"
31 
32 #define GST_CAT_DEFAULT hls_debug
33 
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);
37 
38 GstM3U8 *
gst_m3u8_new(void)39 gst_m3u8_new (void)
40 {
41   GstM3U8 *m3u8;
42 
43   m3u8 = g_new0 (GstM3U8, 1);
44 
45   m3u8->current_file = NULL;
46   m3u8->current_file_duration = GST_CLOCK_TIME_NONE;
47   m3u8->sequence = -1;
48   m3u8->sequence_position = 0;
49   m3u8->highest_sequence_number = -1;
50   m3u8->duration = GST_CLOCK_TIME_NONE;
51 
52   g_mutex_init (&m3u8->lock);
53   m3u8->ref_count = 1;
54 
55   return m3u8;
56 }
57 
58 /* call with M3U8_LOCK held */
59 static void
gst_m3u8_take_uri(GstM3U8 * self,gchar * uri,gchar * base_uri,gchar * name)60 gst_m3u8_take_uri (GstM3U8 * self, gchar * uri, gchar * base_uri, gchar * name)
61 {
62   g_return_if_fail (self != NULL);
63 
64   if (self->uri != uri) {
65     g_free (self->uri);
66     self->uri = uri;
67   }
68   if (self->base_uri != base_uri) {
69     g_free (self->base_uri);
70     self->base_uri = base_uri;
71   }
72   if (self->name != name) {
73     g_free (self->name);
74     self->name = name;
75   }
76 }
77 
78 void
gst_m3u8_set_uri(GstM3U8 * m3u8,const gchar * uri,const gchar * base_uri,const gchar * name)79 gst_m3u8_set_uri (GstM3U8 * m3u8, const gchar * uri, const gchar * base_uri,
80     const gchar * name)
81 {
82   GST_M3U8_LOCK (m3u8);
83   gst_m3u8_take_uri (m3u8, g_strdup (uri), g_strdup (base_uri),
84       g_strdup (name));
85   GST_M3U8_UNLOCK (m3u8);
86 }
87 
88 GstM3U8 *
gst_m3u8_ref(GstM3U8 * m3u8)89 gst_m3u8_ref (GstM3U8 * m3u8)
90 {
91   g_assert (m3u8 != NULL && m3u8->ref_count > 0);
92 
93   g_atomic_int_add (&m3u8->ref_count, 1);
94   return m3u8;
95 }
96 
97 void
gst_m3u8_unref(GstM3U8 * self)98 gst_m3u8_unref (GstM3U8 * self)
99 {
100   g_return_if_fail (self != NULL && self->ref_count > 0);
101 
102   if (g_atomic_int_dec_and_test (&self->ref_count)) {
103     g_free (self->uri);
104     g_free (self->base_uri);
105     g_free (self->name);
106 
107     g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_unref, NULL);
108     g_list_free (self->files);
109 
110     g_free (self->last_data);
111     g_mutex_clear (&self->lock);
112     g_free (self);
113   }
114 }
115 
116 static GstM3U8MediaFile *
gst_m3u8_media_file_new(gchar * uri,gchar * title,GstClockTime duration,guint sequence)117 gst_m3u8_media_file_new (gchar * uri, gchar * title, GstClockTime duration,
118     guint sequence)
119 {
120   GstM3U8MediaFile *file;
121 
122   file = g_new0 (GstM3U8MediaFile, 1);
123   file->uri = uri;
124   file->title = title;
125   file->duration = duration;
126   file->sequence = sequence;
127   file->ref_count = 1;
128 
129   return file;
130 }
131 
132 GstM3U8MediaFile *
gst_m3u8_media_file_ref(GstM3U8MediaFile * mfile)133 gst_m3u8_media_file_ref (GstM3U8MediaFile * mfile)
134 {
135   g_assert (mfile != NULL && mfile->ref_count > 0);
136 
137   g_atomic_int_add (&mfile->ref_count, 1);
138   return mfile;
139 }
140 
141 void
gst_m3u8_media_file_unref(GstM3U8MediaFile * self)142 gst_m3u8_media_file_unref (GstM3U8MediaFile * self)
143 {
144   g_return_if_fail (self != NULL && self->ref_count > 0);
145 
146   if (g_atomic_int_dec_and_test (&self->ref_count)) {
147     g_free (self->title);
148     g_free (self->uri);
149     g_free (self->key);
150     g_free (self);
151   }
152 }
153 
154 static gboolean
int_from_string(gchar * ptr,gchar ** endptr,gint * val)155 int_from_string (gchar * ptr, gchar ** endptr, gint * val)
156 {
157   gchar *end;
158   gint64 ret;
159 
160   g_return_val_if_fail (ptr != NULL, FALSE);
161   g_return_val_if_fail (val != NULL, FALSE);
162 
163   errno = 0;
164   ret = g_ascii_strtoll (ptr, &end, 10);
165   if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64))
166       || (errno != 0 && ret == 0)) {
167     GST_WARNING ("%s", g_strerror (errno));
168     return FALSE;
169   }
170 
171   if (ret > G_MAXINT || ret < G_MININT) {
172     GST_WARNING ("%s", g_strerror (ERANGE));
173     return FALSE;
174   }
175 
176   if (endptr)
177     *endptr = end;
178 
179   *val = (gint) ret;
180 
181   return end != ptr;
182 }
183 
184 static gboolean
int64_from_string(gchar * ptr,gchar ** endptr,gint64 * val)185 int64_from_string (gchar * ptr, gchar ** endptr, gint64 * val)
186 {
187   gchar *end;
188   gint64 ret;
189 
190   g_return_val_if_fail (ptr != NULL, FALSE);
191   g_return_val_if_fail (val != NULL, FALSE);
192 
193   errno = 0;
194   ret = g_ascii_strtoll (ptr, &end, 10);
195   if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64))
196       || (errno != 0 && ret == 0)) {
197     GST_WARNING ("%s", g_strerror (errno));
198     return FALSE;
199   }
200 
201   if (endptr)
202     *endptr = end;
203 
204   *val = ret;
205 
206   return end != ptr;
207 }
208 
209 static gboolean
double_from_string(gchar * ptr,gchar ** endptr,gdouble * val)210 double_from_string (gchar * ptr, gchar ** endptr, gdouble * val)
211 {
212   gchar *end;
213   gdouble ret;
214 
215   g_return_val_if_fail (ptr != NULL, FALSE);
216   g_return_val_if_fail (val != NULL, FALSE);
217 
218   errno = 0;
219   ret = g_ascii_strtod (ptr, &end);
220   if ((errno == ERANGE && (ret == HUGE_VAL || ret == -HUGE_VAL))
221       || (errno != 0 && ret == 0)) {
222     GST_WARNING ("%s", g_strerror (errno));
223     return FALSE;
224   }
225 
226   if (!isfinite (ret)) {
227     GST_WARNING ("%s", g_strerror (ERANGE));
228     return FALSE;
229   }
230 
231   if (endptr)
232     *endptr = end;
233 
234   *val = (gdouble) ret;
235 
236   return end != ptr;
237 }
238 
239 static gboolean
parse_attributes(gchar ** ptr,gchar ** a,gchar ** v)240 parse_attributes (gchar ** ptr, gchar ** a, gchar ** v)
241 {
242   gchar *end = NULL, *p, *ve;
243 
244   g_return_val_if_fail (ptr != NULL, FALSE);
245   g_return_val_if_fail (*ptr != NULL, FALSE);
246   g_return_val_if_fail (a != NULL, FALSE);
247   g_return_val_if_fail (v != NULL, FALSE);
248 
249   /* [attribute=value,]* */
250 
251   *a = *ptr;
252   end = p = g_utf8_strchr (*ptr, -1, ',');
253   if (end) {
254     gchar *q = g_utf8_strchr (*ptr, -1, '"');
255     if (q && q < end) {
256       /* special case, such as CODECS="avc1.77.30, mp4a.40.2" */
257       q = g_utf8_next_char (q);
258       if (q) {
259         q = g_utf8_strchr (q, -1, '"');
260       }
261       if (q) {
262         end = p = g_utf8_strchr (q, -1, ',');
263       }
264     }
265   }
266   if (end) {
267     do {
268       end = g_utf8_next_char (end);
269     } while (end && *end == ' ');
270     *p = '\0';
271   }
272 
273   *v = p = g_utf8_strchr (*ptr, -1, '=');
274   if (*v) {
275     *p = '\0';
276     *v = g_utf8_next_char (*v);
277     if (**v == '"') {
278       ve = g_utf8_next_char (*v);
279       if (ve) {
280         ve = g_utf8_strchr (ve, -1, '"');
281       }
282       if (ve) {
283         *v = g_utf8_next_char (*v);
284         *ve = '\0';
285       } else {
286         GST_WARNING ("Cannot remove quotation marks from %s", *a);
287       }
288     }
289   } else {
290     GST_WARNING ("missing = after attribute");
291     return FALSE;
292   }
293 
294   *ptr = end;
295   return TRUE;
296 }
297 
298 static gint
gst_hls_variant_stream_compare_by_bitrate(gconstpointer a,gconstpointer b)299 gst_hls_variant_stream_compare_by_bitrate (gconstpointer a, gconstpointer b)
300 {
301   const GstHLSVariantStream *vs_a = (const GstHLSVariantStream *) a;
302   const GstHLSVariantStream *vs_b = (const GstHLSVariantStream *) b;
303 
304   if (vs_a->bandwidth == vs_b->bandwidth)
305     return g_strcmp0 (vs_a->name, vs_b->name);
306 
307   return vs_a->bandwidth - vs_b->bandwidth;
308 }
309 
310 /* If we have MEDIA-SEQUENCE, ensure that it's consistent. If it is not,
311  * the client SHOULD halt playback (6.3.4), which is what we do then. */
312 static gboolean
check_media_seqnums(GstM3U8 * self,GList * previous_files)313 check_media_seqnums (GstM3U8 * self, GList * previous_files)
314 {
315   GList *l, *m;
316   GstM3U8MediaFile *f1 = NULL, *f2 = NULL;
317 
318   g_return_val_if_fail (previous_files, FALSE);
319 
320   if (!self->files) {
321     /* Empty playlists are trivially consistent */
322     return TRUE;
323   }
324 
325   /* Find first case of higher/equal sequence number in new playlist.
326    * From there on we can linearly step ahead */
327   for (l = self->files; l; l = l->next) {
328     gboolean match = FALSE;
329 
330     f1 = l->data;
331     for (m = previous_files; m; m = m->next) {
332       f2 = m->data;
333 
334       if (f1->sequence >= f2->sequence) {
335         match = TRUE;
336         break;
337       }
338     }
339     if (match)
340       break;
341   }
342 
343   /* We must have seen at least one entry on each list */
344   g_assert (f1 != NULL);
345   g_assert (f2 != NULL);
346 
347   if (!l) {
348     /* No match, no sequence in the new playlist was higher than
349      * any in the old. This is bad! */
350     GST_ERROR ("Media sequence doesn't continue: last new %" G_GINT64_FORMAT
351         " < last old %" G_GINT64_FORMAT, f1->sequence, f2->sequence);
352     return FALSE;
353   }
354 
355   for (; l && m; l = l->next, m = m->next) {
356     f1 = l->data;
357     f2 = m->data;
358 
359     if (f1->sequence == f2->sequence && !g_str_equal (f1->uri, f2->uri)) {
360       /* Same sequence, different URI. This is bad! */
361       GST_ERROR ("Media URIs inconsistent (sequence %" G_GINT64_FORMAT
362           "): had '%s', got '%s'", f1->sequence, f2->uri, f1->uri);
363       return FALSE;
364     } else if (f1->sequence < f2->sequence) {
365       /* Not same sequence but by construction sequence must be higher in the
366        * new one. All good in that case, if it isn't then this means that
367        * sequence numbers are decreasing or files were inserted */
368       GST_ERROR ("Media sequences inconsistent: %" G_GINT64_FORMAT " < %"
369           G_GINT64_FORMAT ": URIs new '%s' old '%s'", f1->sequence,
370           f2->sequence, f1->uri, f2->uri);
371       return FALSE;
372     }
373   }
374 
375   /* All good if we're getting here */
376   return TRUE;
377 }
378 
379 /* If we don't have MEDIA-SEQUENCE, we check URIs in the previous and
380  * current playlist to calculate the/a correct MEDIA-SEQUENCE for the new
381  * playlist in relation to the old. That is, same URIs get the same number
382  * and later URIs get higher numbers */
383 static void
generate_media_seqnums(GstM3U8 * self,GList * previous_files)384 generate_media_seqnums (GstM3U8 * self, GList * previous_files)
385 {
386   GList *l, *m;
387   GstM3U8MediaFile *f1 = NULL, *f2 = NULL;
388   gint64 mediasequence;
389 
390   g_return_if_fail (previous_files);
391 
392   /* Find first case of same URI in new playlist.
393    * From there on we can linearly step ahead */
394   for (l = self->files; l; l = l->next) {
395     gboolean match = FALSE;
396 
397     f1 = l->data;
398     for (m = previous_files; m; m = m->next) {
399       f2 = m->data;
400 
401       if (g_str_equal (f1->uri, f2->uri)) {
402         match = TRUE;
403         break;
404       }
405     }
406 
407     if (match)
408       break;
409   }
410 
411   if (l) {
412     /* Match, check that all following ones are matching too and continue
413      * sequence numbers from there on */
414 
415     mediasequence = f2->sequence;
416 
417     for (; l && m; l = l->next, m = m->next) {
418       f1 = l->data;
419       f2 = m->data;
420 
421       f1->sequence = mediasequence;
422       mediasequence++;
423 
424       if (!g_str_equal (f1->uri, f2->uri)) {
425         GST_WARNING ("Inconsistent URIs after playlist update: '%s' != '%s'",
426             f1->uri, f2->uri);
427       }
428     }
429   } else {
430     /* No match, this means f2 is the last item in the previous playlist
431      * and we have to start our new playlist at that sequence */
432     mediasequence = f2->sequence + 1;
433     l = self->files;
434   }
435 
436   for (; l; l = l->next) {
437     f1 = l->data;
438 
439     f1->sequence = mediasequence;
440     mediasequence++;
441   }
442 }
443 
444 /*
445  * @data: a m3u8 playlist text data, taking ownership
446  */
447 gboolean
gst_m3u8_update(GstM3U8 * self,gchar * data)448 gst_m3u8_update (GstM3U8 * self, gchar * data)
449 {
450   gint val;
451   GstClockTime duration;
452   gchar *title, *end;
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;
461 
462   g_return_val_if_fail (self != NULL, FALSE);
463   g_return_val_if_fail (data != NULL, FALSE);
464 
465   GST_M3U8_LOCK (self);
466 
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");
470     g_free (data);
471     GST_M3U8_UNLOCK (self);
472     return TRUE;
473   }
474 
475   if (!g_str_has_prefix (data, "#EXTM3U")) {
476     GST_WARNING ("Data doesn't start with #EXTM3U");
477     g_free (data);
478     GST_M3U8_UNLOCK (self);
479     return FALSE;
480   }
481 
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);
485     return FALSE;
486   }
487 
488   GST_TRACE ("data:\n%s", data);
489 
490   g_free (self->last_data);
491   self->last_data = data;
492 
493   self->current_file = NULL;
494   previous_files = self->files;
495   self->files = NULL;
496   self->duration = GST_CLOCK_TIME_NONE;
497   mediasequence = 0;
498 
499   /* By default, allow caching */
500   self->allowcache = TRUE;
501 
502   duration = 0;
503   title = NULL;
504   data += 7;
505   while (TRUE) {
506     gchar *r;
507 
508     end = g_utf8_strchr (data, -1, '\n');
509     if (end)
510       *end = '\0';
511 
512     r = g_utf8_strchr (data, -1, '\r');
513     if (r)
514       *r = '\0';
515 
516     if (data[0] != '#' && data[0] != '\0') {
517       if (duration <= 0) {
518         GST_LOG ("%s: got line without EXTINF, dropping", data);
519         goto next_line;
520       }
521 
522       data = uri_join (self->base_uri ? self->base_uri : self->uri, data);
523       if (data != NULL) {
524         GstM3U8MediaFile *file;
525         file = gst_m3u8_media_file_new (data, title, duration, mediasequence++);
526 
527         /* set encryption params */
528         file->key = current_key ? g_strdup (current_key) : NULL;
529         if (file->key) {
530           if (have_iv) {
531             memcpy (file->iv, iv, sizeof (iv));
532           } else {
533             guint8 *iv = file->iv + 12;
534             GST_WRITE_UINT32_BE (iv, file->sequence);
535           }
536         }
537 
538         if (size != -1) {
539           file->size = size;
540           if (offset != -1) {
541             file->offset = offset;
542           } else {
543             GstM3U8MediaFile *prev = self->files ? self->files->data : NULL;
544 
545             if (!prev) {
546               offset = 0;
547             } else {
548               offset = prev->offset + prev->size;
549             }
550             file->offset = offset;
551           }
552         } else {
553           file->size = -1;
554           file->offset = 0;
555         }
556 
557         file->discont = discontinuity;
558 
559         duration = 0;
560         title = NULL;
561         discontinuity = FALSE;
562         size = offset = -1;
563         self->files = g_list_prepend (self->files, file);
564       }
565 
566     } else if (g_str_has_prefix (data, "#EXTINF:")) {
567       gdouble fval;
568       if (!double_from_string (data + 8, &data, &fval)) {
569         GST_WARNING ("Can't read EXTINF duration");
570         goto next_line;
571       }
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));
577       }
578       if (!data || *data != ',')
579         goto next_line;
580       data = g_utf8_next_char (data);
581       if (data != end) {
582         g_free (title);
583         title = g_strdup (data);
584       }
585     } else if (g_str_has_prefix (data, "#EXT-X-")) {
586       gchar *data_ext_x = data + 7;
587 
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))
593           self->version = 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)) {
599           mediasequence = val;
600           have_mediasequence = TRUE;
601         }
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;
607         }
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:")) {
617         gchar *v, *a;
618 
619         data = data + 11;
620 
621         /* IV and KEY are only valid until the next #EXT-X-KEY */
622         have_iv = FALSE;
623         g_free (current_key);
624         current_key = NULL;
625         while (data && parse_attributes (&data, &a, &v)) {
626           if (g_str_equal (a, "URI")) {
627             current_key =
628                 uri_join (self->base_uri ? self->base_uri : self->uri, v);
629           } else if (g_str_equal (a, "IV")) {
630             gchar *ivp = v;
631             gint i;
632 
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");
636               continue;
637             }
638 
639             ivp += 2;
640             for (i = 0; i < 16; i++) {
641               gint h, l;
642 
643               h = g_ascii_xdigit_value (*ivp);
644               ivp++;
645               l = g_ascii_xdigit_value (*ivp);
646               ivp++;
647               if (h == -1 || l == -1) {
648                 i = -1;
649                 break;
650               }
651               iv[i] = (h << 4) | l;
652             }
653 
654             if (i == -1) {
655               GST_WARNING ("Can't read IV");
656               continue;
657             }
658             have_iv = TRUE;
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);
662               continue;
663             }
664           }
665         }
666       } else if (g_str_has_prefix (data_ext_x, "BYTERANGE:")) {
667         gchar *v = data + 17;
668 
669         if (int64_from_string (v, &v, &size)) {
670           if (*v == '@' && !int64_from_string (v + 1, &v, &offset))
671             goto next_line;
672         } else {
673           goto next_line;
674         }
675       } else {
676         GST_LOG ("Ignored line: %s", data);
677       }
678     } else {
679       GST_LOG ("Ignored line: %s", data);
680     }
681 
682   next_line:
683     if (!end)
684       break;
685     data = g_utf8_next_char (end);      /* skip \n */
686   }
687 
688   g_free (current_key);
689   current_key = NULL;
690 
691   self->files = g_list_reverse (self->files);
692 
693   if (previous_files) {
694     gboolean consistent = TRUE;
695 
696     if (have_mediasequence) {
697       consistent = check_media_seqnums (self, previous_files);
698     } else {
699       generate_media_seqnums (self, previous_files);
700     }
701 
702     g_list_foreach (previous_files, (GFunc) gst_m3u8_media_file_unref, NULL);
703     g_list_free (previous_files);
704     previous_files = NULL;
705 
706     /* error was reported above already */
707     if (!consistent) {
708       GST_M3U8_UNLOCK (self);
709       return FALSE;
710     }
711   }
712 
713   if (self->files == NULL) {
714     GST_ERROR ("Invalid media playlist, it does not contain any media files");
715     GST_M3U8_UNLOCK (self);
716     return FALSE;
717   }
718 
719   /* calculate the start and end times of this media playlist. */
720   {
721     GList *walk;
722     GstM3U8MediaFile *file;
723     GstClockTime duration = 0;
724 
725     mediasequence = -1;
726 
727     for (walk = self->files; walk; walk = walk->next) {
728       file = walk->data;
729 
730       if (mediasequence == -1) {
731         mediasequence = file->sequence;
732       } else if (mediasequence >= file->sequence) {
733         GST_ERROR ("Non-increasing media sequence");
734         GST_M3U8_UNLOCK (self);
735         return FALSE;
736       } else {
737         mediasequence = file->sequence;
738       }
739 
740       duration += file->duration;
741       if (file->sequence > self->highest_sequence_number) {
742         if (self->highest_sequence_number >= 0) {
743           /* if an update of the media playlist has been missed, there
744              will be a gap between self->highest_sequence_number and the
745              first sequence number in this media playlist. In this situation
746              assume that the missing fragments had a duration of
747              targetduration each */
748           self->last_file_end +=
749               (file->sequence - self->highest_sequence_number -
750               1) * self->targetduration;
751         }
752         self->last_file_end += file->duration;
753         self->highest_sequence_number = file->sequence;
754       }
755     }
756     if (GST_M3U8_IS_LIVE (self)) {
757       self->first_file_start = self->last_file_end - duration;
758       GST_DEBUG ("Live playlist range %" GST_TIME_FORMAT " -> %"
759           GST_TIME_FORMAT, GST_TIME_ARGS (self->first_file_start),
760           GST_TIME_ARGS (self->last_file_end));
761     }
762     self->duration = duration;
763   }
764 
765   /* first-time setup */
766   if (self->files && self->sequence == -1) {
767     GList *file;
768 
769     if (GST_M3U8_IS_LIVE (self)) {
770       gint i;
771       GstClockTime sequence_pos = 0;
772 
773       file = g_list_last (self->files);
774 
775       if (self->last_file_end >= GST_M3U8_MEDIA_FILE (file->data)->duration) {
776         sequence_pos =
777             self->last_file_end - GST_M3U8_MEDIA_FILE (file->data)->duration;
778       }
779 
780       /* for live streams, start GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE from
781        * the end of the playlist. See section 6.3.3 of HLS draft */
782       for (i = 0; i < GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE && file->prev &&
783           GST_M3U8_MEDIA_FILE (file->prev->data)->duration <= sequence_pos;
784           ++i) {
785         file = file->prev;
786         sequence_pos -= GST_M3U8_MEDIA_FILE (file->data)->duration;
787       }
788       self->sequence_position = sequence_pos;
789     } else {
790       file = g_list_first (self->files);
791       self->sequence_position = 0;
792     }
793     self->current_file = file;
794     self->sequence = GST_M3U8_MEDIA_FILE (file->data)->sequence;
795     GST_DEBUG ("first sequence: %u", (guint) self->sequence);
796   }
797 
798   GST_LOG ("processed media playlist %s, %u fragments", self->name,
799       g_list_length (self->files));
800 
801   GST_M3U8_UNLOCK (self);
802 
803   return TRUE;
804 }
805 
806 /* call with M3U8_LOCK held */
807 static GList *
m3u8_find_next_fragment(GstM3U8 * m3u8,gboolean forward)808 m3u8_find_next_fragment (GstM3U8 * m3u8, gboolean forward)
809 {
810   GstM3U8MediaFile *file;
811   GList *l = m3u8->files;
812 
813   if (forward) {
814     while (l) {
815       file = l->data;
816 
817       if (file->sequence >= m3u8->sequence)
818         break;
819 
820       l = l->next;
821     }
822   } else {
823     l = g_list_last (l);
824 
825     while (l) {
826       file = l->data;
827 
828       if (file->sequence <= m3u8->sequence)
829         break;
830 
831       l = l->prev;
832     }
833   }
834 
835   return l;
836 }
837 
838 GstM3U8MediaFile *
gst_m3u8_get_next_fragment(GstM3U8 * m3u8,gboolean forward,GstClockTime * sequence_position,gboolean * discont)839 gst_m3u8_get_next_fragment (GstM3U8 * m3u8, gboolean forward,
840     GstClockTime * sequence_position, gboolean * discont)
841 {
842   GstM3U8MediaFile *file = NULL;
843 
844   g_return_val_if_fail (m3u8 != NULL, NULL);
845 
846   GST_M3U8_LOCK (m3u8);
847 
848   GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, m3u8->sequence);
849 
850   if (m3u8->sequence < 0)       /* can't happen really */
851     goto out;
852 
853   if (m3u8->current_file == NULL)
854     m3u8->current_file = m3u8_find_next_fragment (m3u8, forward);
855 
856   if (m3u8->current_file == NULL)
857     goto out;
858 
859   file = gst_m3u8_media_file_ref (m3u8->current_file->data);
860 
861   GST_DEBUG ("Got fragment with sequence %u (current sequence %u)",
862       (guint) file->sequence, (guint) m3u8->sequence);
863 
864   if (sequence_position)
865     *sequence_position = m3u8->sequence_position;
866   if (discont)
867     *discont = file->discont || (m3u8->sequence != file->sequence);
868 
869   m3u8->current_file_duration = file->duration;
870   m3u8->sequence = file->sequence;
871 
872 out:
873 
874   GST_M3U8_UNLOCK (m3u8);
875 
876   return file;
877 }
878 
879 gboolean
gst_m3u8_has_next_fragment(GstM3U8 * m3u8,gboolean forward)880 gst_m3u8_has_next_fragment (GstM3U8 * m3u8, gboolean forward)
881 {
882   gboolean have_next;
883   GList *cur;
884 
885   g_return_val_if_fail (m3u8 != NULL, FALSE);
886 
887   GST_M3U8_LOCK (m3u8);
888 
889   GST_DEBUG ("Checking next fragment %" G_GINT64_FORMAT,
890       m3u8->sequence + (forward ? 1 : -1));
891 
892   if (m3u8->current_file) {
893     cur = m3u8->current_file;
894   } else {
895     cur = m3u8_find_next_fragment (m3u8, forward);
896   }
897 
898   have_next = cur && ((forward && cur->next) || (!forward && cur->prev));
899 
900   GST_M3U8_UNLOCK (m3u8);
901 
902   return have_next;
903 }
904 
905 /* call with M3U8_LOCK held */
906 static void
m3u8_alternate_advance(GstM3U8 * m3u8,gboolean forward)907 m3u8_alternate_advance (GstM3U8 * m3u8, gboolean forward)
908 {
909   gint targetnum = m3u8->sequence;
910   GList *tmp;
911   GstM3U8MediaFile *mf;
912 
913   /* figure out the target seqnum */
914   if (forward)
915     targetnum += 1;
916   else
917     targetnum -= 1;
918 
919   for (tmp = m3u8->files; tmp; tmp = tmp->next) {
920     mf = (GstM3U8MediaFile *) tmp->data;
921     if (mf->sequence == targetnum)
922       break;
923   }
924   if (tmp == NULL) {
925     GST_WARNING ("Can't find next fragment");
926     return;
927   }
928   m3u8->current_file = tmp;
929   m3u8->sequence = targetnum;
930   m3u8->current_file_duration = GST_M3U8_MEDIA_FILE (tmp->data)->duration;
931 }
932 
933 void
gst_m3u8_advance_fragment(GstM3U8 * m3u8,gboolean forward)934 gst_m3u8_advance_fragment (GstM3U8 * m3u8, gboolean forward)
935 {
936   GstM3U8MediaFile *file;
937 
938   g_return_if_fail (m3u8 != NULL);
939 
940   GST_M3U8_LOCK (m3u8);
941 
942   GST_DEBUG ("Sequence position was %" GST_TIME_FORMAT,
943       GST_TIME_ARGS (m3u8->sequence_position));
944   if (GST_CLOCK_TIME_IS_VALID (m3u8->current_file_duration)) {
945     /* Advance our position based on the previous fragment we played */
946     if (forward)
947       m3u8->sequence_position += m3u8->current_file_duration;
948     else if (m3u8->current_file_duration < m3u8->sequence_position)
949       m3u8->sequence_position -= m3u8->current_file_duration;
950     else
951       m3u8->sequence_position = 0;
952     m3u8->current_file_duration = GST_CLOCK_TIME_NONE;
953     GST_DEBUG ("Sequence position now %" GST_TIME_FORMAT,
954         GST_TIME_ARGS (m3u8->sequence_position));
955   }
956   if (!m3u8->current_file) {
957     GList *l;
958 
959     GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, m3u8->sequence);
960     for (l = m3u8->files; l != NULL; l = l->next) {
961       if (GST_M3U8_MEDIA_FILE (l->data)->sequence == m3u8->sequence) {
962         m3u8->current_file = l;
963         break;
964       }
965     }
966     if (m3u8->current_file == NULL) {
967       GST_DEBUG
968           ("Could not find current fragment, trying next fragment directly");
969       m3u8_alternate_advance (m3u8, forward);
970 
971       /* Resync sequence number if the above has failed for live streams */
972       if (m3u8->current_file == NULL && GST_M3U8_IS_LIVE (m3u8)) {
973         /* for live streams, start GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE from
974            the end of the playlist. See section 6.3.3 of HLS draft */
975         gint pos =
976             g_list_length (m3u8->files) - GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE;
977         m3u8->current_file = g_list_nth (m3u8->files, pos >= 0 ? pos : 0);
978         m3u8->current_file_duration =
979             GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->duration;
980 
981         GST_WARNING ("Resyncing live playlist");
982       }
983       goto out;
984     }
985   }
986 
987   file = GST_M3U8_MEDIA_FILE (m3u8->current_file->data);
988   GST_DEBUG ("Advancing from sequence %u", (guint) file->sequence);
989   if (forward) {
990     m3u8->current_file = m3u8->current_file->next;
991     if (m3u8->current_file) {
992       m3u8->sequence = GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->sequence;
993     } else {
994       m3u8->sequence = file->sequence + 1;
995     }
996   } else {
997     m3u8->current_file = m3u8->current_file->prev;
998     if (m3u8->current_file) {
999       m3u8->sequence = GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->sequence;
1000     } else {
1001       m3u8->sequence = file->sequence - 1;
1002     }
1003   }
1004   if (m3u8->current_file) {
1005     /* Store duration of the fragment we're using to update the position
1006      * the next time we advance */
1007     m3u8->current_file_duration =
1008         GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->duration;
1009   }
1010 
1011 out:
1012 
1013   GST_M3U8_UNLOCK (m3u8);
1014 }
1015 
1016 GstClockTime
gst_m3u8_get_duration(GstM3U8 * m3u8)1017 gst_m3u8_get_duration (GstM3U8 * m3u8)
1018 {
1019   GstClockTime duration = GST_CLOCK_TIME_NONE;
1020 
1021   g_return_val_if_fail (m3u8 != NULL, GST_CLOCK_TIME_NONE);
1022 
1023   GST_M3U8_LOCK (m3u8);
1024 
1025   /* We can only get the duration for on-demand streams */
1026   if (!m3u8->endlist)
1027     goto out;
1028 
1029   if (!GST_CLOCK_TIME_IS_VALID (m3u8->duration) && m3u8->files != NULL) {
1030     GList *f;
1031 
1032     m3u8->duration = 0;
1033     for (f = m3u8->files; f != NULL; f = f->next)
1034       m3u8->duration += GST_M3U8_MEDIA_FILE (f)->duration;
1035   }
1036   duration = m3u8->duration;
1037 
1038 out:
1039 
1040   GST_M3U8_UNLOCK (m3u8);
1041 
1042   return duration;
1043 }
1044 
1045 GstClockTime
gst_m3u8_get_target_duration(GstM3U8 * m3u8)1046 gst_m3u8_get_target_duration (GstM3U8 * m3u8)
1047 {
1048   GstClockTime target_duration;
1049 
1050   g_return_val_if_fail (m3u8 != NULL, GST_CLOCK_TIME_NONE);
1051 
1052   GST_M3U8_LOCK (m3u8);
1053   target_duration = m3u8->targetduration;
1054   GST_M3U8_UNLOCK (m3u8);
1055 
1056   return target_duration;
1057 }
1058 
1059 gchar *
gst_m3u8_get_uri(GstM3U8 * m3u8)1060 gst_m3u8_get_uri (GstM3U8 * m3u8)
1061 {
1062   gchar *uri;
1063 
1064   GST_M3U8_LOCK (m3u8);
1065   uri = g_strdup (m3u8->uri);
1066   GST_M3U8_UNLOCK (m3u8);
1067 
1068   return uri;
1069 }
1070 
1071 gboolean
gst_m3u8_is_live(GstM3U8 * m3u8)1072 gst_m3u8_is_live (GstM3U8 * m3u8)
1073 {
1074   gboolean is_live;
1075 
1076   g_return_val_if_fail (m3u8 != NULL, FALSE);
1077 
1078   GST_M3U8_LOCK (m3u8);
1079   is_live = GST_M3U8_IS_LIVE (m3u8);
1080   GST_M3U8_UNLOCK (m3u8);
1081 
1082   return is_live;
1083 }
1084 
1085 gchar *
uri_join(const gchar * uri1,const gchar * uri2)1086 uri_join (const gchar * uri1, const gchar * uri2)
1087 {
1088   gchar *uri_copy, *tmp, *ret = NULL;
1089 
1090   if (gst_uri_is_valid (uri2))
1091     return g_strdup (uri2);
1092 
1093   uri_copy = g_strdup (uri1);
1094   if (uri2[0] != '/') {
1095     /* uri2 is a relative uri2 */
1096     /* look for query params */
1097     tmp = g_utf8_strchr (uri_copy, -1, '?');
1098     if (tmp) {
1099       /* find last / char, ignoring query params */
1100       tmp = g_utf8_strrchr (uri_copy, tmp - uri_copy, '/');
1101     } else {
1102       /* find last / char in URL */
1103       tmp = g_utf8_strrchr (uri_copy, -1, '/');
1104     }
1105     if (!tmp) {
1106       GST_WARNING ("Can't build a valid uri_copy");
1107       goto out;
1108     }
1109 
1110     *tmp = '\0';
1111     ret = g_strdup_printf ("%s/%s", uri_copy, uri2);
1112   } else {
1113     /* uri2 is an absolute uri2 */
1114     char *scheme, *hostname;
1115 
1116     scheme = uri_copy;
1117     /* find the : in <scheme>:// */
1118     tmp = g_utf8_strchr (uri_copy, -1, ':');
1119     if (!tmp) {
1120       GST_WARNING ("Can't build a valid uri_copy");
1121       goto out;
1122     }
1123 
1124     *tmp = '\0';
1125 
1126     /* skip :// */
1127     hostname = tmp + 3;
1128 
1129     tmp = g_utf8_strchr (hostname, -1, '/');
1130     if (tmp)
1131       *tmp = '\0';
1132 
1133     ret = g_strdup_printf ("%s://%s%s", scheme, hostname, uri2);
1134   }
1135 
1136 out:
1137   g_free (uri_copy);
1138   return ret;
1139 }
1140 
1141 gboolean
gst_m3u8_get_seek_range(GstM3U8 * m3u8,gint64 * start,gint64 * stop)1142 gst_m3u8_get_seek_range (GstM3U8 * m3u8, gint64 * start, gint64 * stop)
1143 {
1144   GstClockTime duration = 0;
1145   GList *walk;
1146   GstM3U8MediaFile *file;
1147   guint count;
1148   guint min_distance = 0;
1149 
1150   g_return_val_if_fail (m3u8 != NULL, FALSE);
1151 
1152   GST_M3U8_LOCK (m3u8);
1153 
1154   if (m3u8->files == NULL)
1155     goto out;
1156 
1157   if (GST_M3U8_IS_LIVE (m3u8)) {
1158     /* min_distance is used to make sure the seek range is never closer than
1159        GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE fragments from the end of a live
1160        playlist - see 6.3.3. "Playing the Playlist file" of the HLS draft */
1161     min_distance = GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE;
1162   }
1163   count = g_list_length (m3u8->files);
1164 
1165   for (walk = m3u8->files; walk && count > min_distance; walk = walk->next) {
1166     file = walk->data;
1167     --count;
1168     duration += file->duration;
1169   }
1170 
1171   if (duration <= 0)
1172     goto out;
1173 
1174   *start = m3u8->first_file_start;
1175   *stop = *start + duration;
1176 
1177 out:
1178 
1179   GST_M3U8_UNLOCK (m3u8);
1180   return (duration > 0);
1181 }
1182 
1183 GstHLSMedia *
gst_hls_media_ref(GstHLSMedia * media)1184 gst_hls_media_ref (GstHLSMedia * media)
1185 {
1186   g_assert (media != NULL && media->ref_count > 0);
1187   g_atomic_int_add (&media->ref_count, 1);
1188   return media;
1189 }
1190 
1191 void
gst_hls_media_unref(GstHLSMedia * media)1192 gst_hls_media_unref (GstHLSMedia * media)
1193 {
1194   g_assert (media != NULL && media->ref_count > 0);
1195   if (g_atomic_int_dec_and_test (&media->ref_count)) {
1196     if (media->playlist)
1197       gst_m3u8_unref (media->playlist);
1198     g_free (media->group_id);
1199     g_free (media->name);
1200     g_free (media->uri);
1201     g_free (media->lang);
1202     g_free (media);
1203   }
1204 }
1205 
1206 static GstHLSMediaType
gst_m3u8_get_hls_media_type_from_string(const gchar * type_name)1207 gst_m3u8_get_hls_media_type_from_string (const gchar * type_name)
1208 {
1209   if (strcmp (type_name, "AUDIO") == 0)
1210     return GST_HLS_MEDIA_TYPE_AUDIO;
1211   if (strcmp (type_name, "VIDEO") == 0)
1212     return GST_HLS_MEDIA_TYPE_VIDEO;
1213   if (strcmp (type_name, "SUBTITLES") == 0)
1214     return GST_HLS_MEDIA_TYPE_SUBTITLES;
1215   if (strcmp (type_name, "CLOSED_CAPTIONS") == 0)
1216     return GST_HLS_MEDIA_TYPE_CLOSED_CAPTIONS;
1217 
1218   return GST_HLS_MEDIA_TYPE_INVALID;
1219 }
1220 
1221 #define GST_HLS_MEDIA_TYPE_NAME(mtype) gst_m3u8_hls_media_type_get_nick(mtype)
1222 static inline const gchar *
gst_m3u8_hls_media_type_get_nick(GstHLSMediaType mtype)1223 gst_m3u8_hls_media_type_get_nick (GstHLSMediaType mtype)
1224 {
1225   static const gchar *nicks[GST_HLS_N_MEDIA_TYPES] = { "audio", "video",
1226     "subtitle", "closed-captions"
1227   };
1228 
1229   if (mtype < 0 || mtype >= GST_HLS_N_MEDIA_TYPES)
1230     return "invalid";
1231 
1232   return nicks[mtype];
1233 }
1234 
1235 /* returns unquoted copy of string */
1236 static gchar *
gst_m3u8_unquote(const gchar * str)1237 gst_m3u8_unquote (const gchar * str)
1238 {
1239   const gchar *start, *end;
1240 
1241   start = strchr (str, '"');
1242   if (start == NULL)
1243     return g_strdup (str);
1244   end = strchr (start + 1, '"');
1245   if (end == NULL) {
1246     GST_WARNING ("Broken quoted string [%s] - can't find end quote", str);
1247     return g_strdup (start + 1);
1248   }
1249   return g_strndup (start + 1, (gsize) (end - (start + 1)));
1250 }
1251 
1252 static GstHLSMedia *
gst_m3u8_parse_media(gchar * desc,const gchar * base_uri)1253 gst_m3u8_parse_media (gchar * desc, const gchar * base_uri)
1254 {
1255   GstHLSMedia *media;
1256   gchar *a, *v;
1257 
1258   media = g_new0 (GstHLSMedia, 1);
1259   media->ref_count = 1;
1260   media->playlist = gst_m3u8_new ();
1261   media->mtype = GST_HLS_MEDIA_TYPE_INVALID;
1262 
1263   GST_LOG ("parsing %s", desc);
1264   while (desc != NULL && parse_attributes (&desc, &a, &v)) {
1265     if (strcmp (a, "TYPE") == 0) {
1266       media->mtype = gst_m3u8_get_hls_media_type_from_string (v);
1267     } else if (strcmp (a, "GROUP-ID") == 0) {
1268       g_free (media->group_id);
1269       media->group_id = gst_m3u8_unquote (v);
1270     } else if (strcmp (a, "NAME") == 0) {
1271       g_free (media->name);
1272       media->name = gst_m3u8_unquote (v);
1273     } else if (strcmp (a, "URI") == 0) {
1274       gchar *uri;
1275 
1276       g_free (media->uri);
1277       uri = gst_m3u8_unquote (v);
1278       media->uri = uri_join (base_uri, uri);
1279       g_free (uri);
1280     } else if (strcmp (a, "LANGUAGE") == 0) {
1281       g_free (media->lang);
1282       media->lang = gst_m3u8_unquote (v);
1283     } else if (strcmp (a, "DEFAULT") == 0) {
1284       media->is_default = g_ascii_strcasecmp (v, "yes") == 0;
1285     } else if (strcmp (a, "FORCED") == 0) {
1286       media->forced = g_ascii_strcasecmp (v, "yes") == 0;
1287     } else if (strcmp (a, "AUTOSELECT") == 0) {
1288       media->autoselect = g_ascii_strcasecmp (v, "yes") == 0;
1289     } else {
1290       /* unhandled: ASSOC-LANGUAGE, INSTREAM-ID, CHARACTERISTICS */
1291       GST_FIXME ("EXT-X-MEDIA: unhandled attribute: %s = %s", a, v);
1292     }
1293   }
1294 
1295   if (media->mtype == GST_HLS_MEDIA_TYPE_INVALID)
1296     goto required_attributes_missing;
1297 
1298   if (media->uri == NULL)
1299     goto existing_stream;
1300 
1301   if (media->group_id == NULL || media->name == NULL)
1302     goto required_attributes_missing;
1303 
1304   if (media->mtype == GST_HLS_MEDIA_TYPE_CLOSED_CAPTIONS)
1305     goto uri_with_cc;
1306 
1307   GST_DEBUG ("media: %s, group '%s', name '%s', uri '%s', %s %s %s, lang=%s",
1308       GST_HLS_MEDIA_TYPE_NAME (media->mtype), media->group_id, media->name,
1309       media->uri, media->is_default ? "default" : "-",
1310       media->autoselect ? "autoselect" : "-",
1311       media->forced ? "forced" : "-", media->lang ? media->lang : "??");
1312 
1313   return media;
1314 
1315 uri_with_cc:
1316   {
1317     GST_WARNING ("closed captions EXT-X-MEDIA should not have URI specified");
1318     goto out_error;
1319   }
1320 required_attributes_missing:
1321   {
1322     GST_WARNING ("EXT-X-MEDIA description is missing required attributes");
1323     goto out_error;
1324     /* fall through */
1325   }
1326 existing_stream:
1327   {
1328     GST_DEBUG ("EXT-X-MEDIA without URI, describes embedded stream, skipping");
1329     /* fall through */
1330   }
1331 
1332 out_error:
1333   {
1334     gst_hls_media_unref (media);
1335     return NULL;
1336   }
1337 }
1338 
1339 static GstHLSVariantStream *
gst_hls_variant_stream_new(void)1340 gst_hls_variant_stream_new (void)
1341 {
1342   GstHLSVariantStream *stream;
1343 
1344   stream = g_new0 (GstHLSVariantStream, 1);
1345   stream->m3u8 = gst_m3u8_new ();
1346   stream->refcount = 1;
1347   return stream;
1348 }
1349 
1350 GstHLSVariantStream *
gst_hls_variant_stream_ref(GstHLSVariantStream * stream)1351 gst_hls_variant_stream_ref (GstHLSVariantStream * stream)
1352 {
1353   g_atomic_int_inc (&stream->refcount);
1354   return stream;
1355 }
1356 
1357 void
gst_hls_variant_stream_unref(GstHLSVariantStream * stream)1358 gst_hls_variant_stream_unref (GstHLSVariantStream * stream)
1359 {
1360   if (g_atomic_int_dec_and_test (&stream->refcount)) {
1361     gint i;
1362 
1363     g_free (stream->name);
1364     g_free (stream->uri);
1365     g_free (stream->codecs);
1366     gst_m3u8_unref (stream->m3u8);
1367     for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
1368       g_free (stream->media_groups[i]);
1369       g_list_free_full (stream->media[i], (GDestroyNotify) gst_hls_media_unref);
1370     }
1371     g_free (stream);
1372   }
1373 }
1374 
1375 static GstHLSVariantStream *
find_variant_stream_by_name(GList * list,const gchar * name)1376 find_variant_stream_by_name (GList * list, const gchar * name)
1377 {
1378   for (; list != NULL; list = list->next) {
1379     GstHLSVariantStream *variant_stream = list->data;
1380 
1381     if (variant_stream->name != NULL && !strcmp (variant_stream->name, name))
1382       return variant_stream;
1383   }
1384   return NULL;
1385 }
1386 
1387 static GstHLSVariantStream *
find_variant_stream_by_uri(GList * list,const gchar * uri)1388 find_variant_stream_by_uri (GList * list, const gchar * uri)
1389 {
1390   for (; list != NULL; list = list->next) {
1391     GstHLSVariantStream *variant_stream = list->data;
1392 
1393     if (variant_stream->uri != NULL && !strcmp (variant_stream->uri, uri))
1394       return variant_stream;
1395   }
1396   return NULL;
1397 }
1398 
1399 static GstHLSMasterPlaylist *
gst_hls_master_playlist_new(void)1400 gst_hls_master_playlist_new (void)
1401 {
1402   GstHLSMasterPlaylist *playlist;
1403 
1404   playlist = g_new0 (GstHLSMasterPlaylist, 1);
1405   playlist->refcount = 1;
1406   playlist->is_simple = FALSE;
1407 
1408   return playlist;
1409 }
1410 
1411 void
gst_hls_master_playlist_unref(GstHLSMasterPlaylist * playlist)1412 gst_hls_master_playlist_unref (GstHLSMasterPlaylist * playlist)
1413 {
1414   if (g_atomic_int_dec_and_test (&playlist->refcount)) {
1415     g_list_free_full (playlist->variants,
1416         (GDestroyNotify) gst_hls_variant_stream_unref);
1417     g_list_free_full (playlist->iframe_variants,
1418         (GDestroyNotify) gst_hls_variant_stream_unref);
1419     if (playlist->default_variant)
1420       gst_hls_variant_stream_unref (playlist->default_variant);
1421     g_free (playlist->last_data);
1422     g_free (playlist);
1423   }
1424 }
1425 
1426 static gint
hls_media_name_compare_func(gconstpointer media,gconstpointer name)1427 hls_media_name_compare_func (gconstpointer media, gconstpointer name)
1428 {
1429   return strcmp (((GstHLSMedia *) media)->name, (const gchar *) name);
1430 }
1431 
1432 /* Takes ownership of @data */
1433 GstHLSMasterPlaylist *
gst_hls_master_playlist_new_from_data(gchar * data,const gchar * base_uri)1434 gst_hls_master_playlist_new_from_data (gchar * data, const gchar * base_uri)
1435 {
1436   GHashTable *media_groups[GST_HLS_N_MEDIA_TYPES] = { NULL, };
1437   GstHLSMasterPlaylist *playlist;
1438   GstHLSVariantStream *pending_stream;
1439   gchar *end, *free_data = data;
1440   gint val, i;
1441   GList *l;
1442 
1443   if (!g_str_has_prefix (data, "#EXTM3U")) {
1444     GST_WARNING ("Data doesn't start with #EXTM3U");
1445     g_free (free_data);
1446     return NULL;
1447   }
1448 
1449   playlist = gst_hls_master_playlist_new ();
1450 
1451   /* store data before we modify it for parsing */
1452   playlist->last_data = g_strdup (data);
1453 
1454   GST_TRACE ("data:\n%s", data);
1455 
1456   if (strstr (data, "\n#EXTINF:") != NULL) {
1457     GST_INFO ("This is a simple media playlist, not a master playlist");
1458 
1459     pending_stream = gst_hls_variant_stream_new ();
1460     pending_stream->name = g_strdup (base_uri);
1461     pending_stream->uri = g_strdup (base_uri);
1462     gst_m3u8_set_uri (pending_stream->m3u8, base_uri, NULL, base_uri);
1463     playlist->variants = g_list_append (playlist->variants, pending_stream);
1464     playlist->default_variant = gst_hls_variant_stream_ref (pending_stream);
1465     playlist->is_simple = TRUE;
1466 
1467     if (!gst_m3u8_update (pending_stream->m3u8, data)) {
1468       GST_WARNING ("Failed to parse media playlist");
1469       gst_hls_master_playlist_unref (playlist);
1470       playlist = NULL;
1471     }
1472     return playlist;
1473   }
1474 
1475   pending_stream = NULL;
1476   data += 7;
1477   while (TRUE) {
1478     gchar *r;
1479 
1480     end = g_utf8_strchr (data, -1, '\n');
1481     if (end)
1482       *end = '\0';
1483 
1484     r = g_utf8_strchr (data, -1, '\r');
1485     if (r)
1486       *r = '\0';
1487 
1488     if (data[0] != '#' && data[0] != '\0') {
1489       gchar *name, *uri;
1490 
1491       if (pending_stream == NULL) {
1492         GST_LOG ("%s: got line without EXT-STREAM-INF, dropping", data);
1493         goto next_line;
1494       }
1495 
1496       name = data;
1497       uri = uri_join (base_uri, name);
1498       if (uri == NULL)
1499         goto next_line;
1500 
1501       pending_stream->name = g_strdup (name);
1502       pending_stream->uri = uri;
1503 
1504       if (find_variant_stream_by_name (playlist->variants, name)
1505           || find_variant_stream_by_uri (playlist->variants, uri)) {
1506         GST_DEBUG ("Already have a list with this name or URI: %s", name);
1507         gst_hls_variant_stream_unref (pending_stream);
1508       } else {
1509         GST_INFO ("stream %s @ %u: %s", name, pending_stream->bandwidth, uri);
1510         gst_m3u8_set_uri (pending_stream->m3u8, uri, NULL, name);
1511         playlist->variants = g_list_append (playlist->variants, pending_stream);
1512         /* use first stream in the playlist as default */
1513         if (playlist->default_variant == NULL) {
1514           playlist->default_variant =
1515               gst_hls_variant_stream_ref (pending_stream);
1516         }
1517       }
1518       pending_stream = NULL;
1519     } else if (g_str_has_prefix (data, "#EXT-X-VERSION:")) {
1520       if (int_from_string (data + 15, &data, &val))
1521         playlist->version = val;
1522     } else if (g_str_has_prefix (data, "#EXT-X-STREAM-INF:") ||
1523         g_str_has_prefix (data, "#EXT-X-I-FRAME-STREAM-INF:")) {
1524       GstHLSVariantStream *stream;
1525       gchar *v, *a;
1526 
1527       stream = gst_hls_variant_stream_new ();
1528       stream->iframe = g_str_has_prefix (data, "#EXT-X-I-FRAME-STREAM-INF:");
1529       data += stream->iframe ? 26 : 18;
1530       while (data && parse_attributes (&data, &a, &v)) {
1531         if (g_str_equal (a, "BANDWIDTH")) {
1532           if (!stream->bandwidth) {
1533             if (!int_from_string (v, NULL, &stream->bandwidth))
1534               GST_WARNING ("Error while reading BANDWIDTH");
1535           }
1536         } else if (g_str_equal (a, "AVERAGE-BANDWIDTH")) {
1537           GST_DEBUG
1538               ("AVERAGE-BANDWIDTH attribute available. Using it as stream bandwidth");
1539           if (!int_from_string (v, NULL, &stream->bandwidth))
1540             GST_WARNING ("Error while reading AVERAGE-BANDWIDTH");
1541         } else if (g_str_equal (a, "PROGRAM-ID")) {
1542           if (!int_from_string (v, NULL, &stream->program_id))
1543             GST_WARNING ("Error while reading PROGRAM-ID");
1544         } else if (g_str_equal (a, "CODECS")) {
1545           g_free (stream->codecs);
1546           stream->codecs = g_strdup (v);
1547         } else if (g_str_equal (a, "RESOLUTION")) {
1548           if (!int_from_string (v, &v, &stream->width))
1549             GST_WARNING ("Error while reading RESOLUTION width");
1550           if (!v || *v != 'x') {
1551             GST_WARNING ("Missing height");
1552           } else {
1553             v = g_utf8_next_char (v);
1554             if (!int_from_string (v, NULL, &stream->height))
1555               GST_WARNING ("Error while reading RESOLUTION height");
1556           }
1557         } else if (stream->iframe && g_str_equal (a, "URI")) {
1558           stream->uri = uri_join (base_uri, v);
1559           if (stream->uri != NULL) {
1560             stream->name = g_strdup (stream->uri);
1561             gst_m3u8_set_uri (stream->m3u8, stream->uri, NULL, stream->name);
1562           } else {
1563             gst_hls_variant_stream_unref (stream);
1564           }
1565         } else if (g_str_equal (a, "AUDIO")) {
1566           g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_AUDIO]);
1567           stream->media_groups[GST_HLS_MEDIA_TYPE_AUDIO] = gst_m3u8_unquote (v);
1568         } else if (g_str_equal (a, "SUBTITLES")) {
1569           g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_SUBTITLES]);
1570           stream->media_groups[GST_HLS_MEDIA_TYPE_SUBTITLES] =
1571               gst_m3u8_unquote (v);
1572         } else if (g_str_equal (a, "VIDEO")) {
1573           g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_VIDEO]);
1574           stream->media_groups[GST_HLS_MEDIA_TYPE_VIDEO] = gst_m3u8_unquote (v);
1575         } else if (g_str_equal (a, "CLOSED-CAPTIONS")) {
1576           /* closed captions will be embedded inside the video stream, ignore */
1577         }
1578       }
1579 
1580       if (stream->iframe) {
1581         if (find_variant_stream_by_uri (playlist->iframe_variants, stream->uri)) {
1582           GST_DEBUG ("Already have a list with this URI");
1583           gst_hls_variant_stream_unref (stream);
1584         } else {
1585           playlist->iframe_variants =
1586               g_list_append (playlist->iframe_variants, stream);
1587         }
1588       } else {
1589         if (pending_stream != NULL) {
1590           GST_WARNING ("variant stream without uri, dropping");
1591           gst_hls_variant_stream_unref (pending_stream);
1592         }
1593         pending_stream = stream;
1594       }
1595     } else if (g_str_has_prefix (data, "#EXT-X-MEDIA:")) {
1596       GstHLSMedia *media;
1597       GList *list;
1598 
1599       media = gst_m3u8_parse_media (data + strlen ("#EXT-X-MEDIA:"), base_uri);
1600 
1601       if (media == NULL)
1602         goto next_line;
1603 
1604       if (media_groups[media->mtype] == NULL) {
1605         media_groups[media->mtype] =
1606             g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
1607       }
1608 
1609       list = g_hash_table_lookup (media_groups[media->mtype], media->group_id);
1610 
1611       /* make sure there isn't already a media with the same name */
1612       if (!g_list_find_custom (list, media->name, hls_media_name_compare_func)) {
1613         g_hash_table_replace (media_groups[media->mtype],
1614             g_strdup (media->group_id), g_list_append (list, media));
1615         GST_INFO ("Added media %s to group %s", media->name, media->group_id);
1616       } else {
1617         GST_WARNING ("  media with name '%s' already exists in group '%s'!",
1618             media->name, media->group_id);
1619         gst_hls_media_unref (media);
1620       }
1621     } else if (*data != '\0') {
1622       GST_LOG ("Ignored line: %s", data);
1623     }
1624 
1625   next_line:
1626     if (!end)
1627       break;
1628     data = g_utf8_next_char (end);      /* skip \n */
1629   }
1630 
1631   if (pending_stream != NULL) {
1632     GST_WARNING ("#EXT-X-STREAM-INF without uri, dropping");
1633     gst_hls_variant_stream_unref (pending_stream);
1634   }
1635 
1636   g_free (free_data);
1637 
1638   /* Add alternative renditions media to variant streams */
1639   for (l = playlist->variants; l != NULL; l = l->next) {
1640     GstHLSVariantStream *stream = l->data;
1641     GList *mlist;
1642 
1643     for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
1644       if (stream->media_groups[i] != NULL && media_groups[i] != NULL) {
1645         GST_INFO ("Adding %s group '%s' to stream '%s'",
1646             GST_HLS_MEDIA_TYPE_NAME (i), stream->media_groups[i], stream->name);
1647 
1648         mlist = g_hash_table_lookup (media_groups[i], stream->media_groups[i]);
1649 
1650         if (mlist == NULL)
1651           GST_WARNING ("Group '%s' does not exist!", stream->media_groups[i]);
1652 
1653         while (mlist != NULL) {
1654           GstHLSMedia *media = mlist->data;
1655 
1656           GST_DEBUG ("  %s media %s, uri: %s", GST_HLS_MEDIA_TYPE_NAME (i),
1657               media->name, media->uri);
1658 
1659           stream->media[i] =
1660               g_list_append (stream->media[i], gst_hls_media_ref (media));
1661           mlist = mlist->next;
1662         }
1663       }
1664     }
1665   }
1666 
1667   /* clean up our temporary alternative rendition groups hash tables */
1668   for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
1669     if (media_groups[i] != NULL) {
1670       GList *groups, *mlist;
1671 
1672       groups = g_hash_table_get_keys (media_groups[i]);
1673       for (l = groups; l != NULL; l = l->next) {
1674         mlist = g_hash_table_lookup (media_groups[i], l->data);
1675         g_list_free_full (mlist, (GDestroyNotify) gst_hls_media_unref);
1676       }
1677       g_list_free (groups);
1678       g_hash_table_unref (media_groups[i]);
1679     }
1680   }
1681 
1682   if (playlist->variants == NULL) {
1683     GST_WARNING ("Master playlist without any media playlists!");
1684     gst_hls_master_playlist_unref (playlist);
1685     return NULL;
1686   }
1687 
1688   /* reorder variants by bitrate */
1689   playlist->variants =
1690       g_list_sort (playlist->variants,
1691       (GCompareFunc) gst_hls_variant_stream_compare_by_bitrate);
1692 
1693   playlist->iframe_variants =
1694       g_list_sort (playlist->iframe_variants,
1695       (GCompareFunc) gst_hls_variant_stream_compare_by_bitrate);
1696 
1697   /* FIXME: restore old current_variant after master playlist update
1698    * (move into code that does that update) */
1699 #if 0
1700   {
1701     gchar *top_variant_uri = NULL;
1702     gboolean iframe = FALSE;
1703 
1704     if (!self->current_variant) {
1705       top_variant_uri = GST_M3U8 (self->lists->data)->uri;
1706     } else {
1707       top_variant_uri = GST_M3U8 (self->current_variant->data)->uri;
1708       iframe = GST_M3U8 (self->current_variant->data)->iframe;
1709     }
1710 
1711     /* here we sorted the lists */
1712 
1713     if (iframe)
1714       playlist->current_variant =
1715           find_variant_stream_by_uri (playlist->iframe_variants,
1716           top_variant_uri);
1717     else
1718       playlist->current_variant =
1719           find_variant_stream_by_uri (playlist->variants, top_variant_uri);
1720   }
1721 #endif
1722 
1723   GST_DEBUG ("parsed master playlist with %d streams and %d I-frame streams",
1724       g_list_length (playlist->variants),
1725       g_list_length (playlist->iframe_variants));
1726 
1727 
1728   return playlist;
1729 }
1730 
1731 gboolean
gst_hls_variant_stream_is_live(GstHLSVariantStream * variant)1732 gst_hls_variant_stream_is_live (GstHLSVariantStream * variant)
1733 {
1734   gboolean is_live;
1735 
1736   g_return_val_if_fail (variant != NULL, FALSE);
1737 
1738   is_live = gst_m3u8_is_live (variant->m3u8);
1739 
1740   return is_live;
1741 }
1742 
1743 static gint
compare_media(const GstHLSMedia * a,const GstHLSMedia * b)1744 compare_media (const GstHLSMedia * a, const GstHLSMedia * b)
1745 {
1746   return strcmp (a->name, b->name);
1747 }
1748 
1749 GstHLSMedia *
gst_hls_variant_find_matching_media(GstHLSVariantStream * stream,GstHLSMedia * media)1750 gst_hls_variant_find_matching_media (GstHLSVariantStream * stream,
1751     GstHLSMedia * media)
1752 {
1753   GList *mlist = stream->media[media->mtype];
1754   GList *match;
1755 
1756   if (mlist == NULL)
1757     return NULL;
1758 
1759   match = g_list_find_custom (mlist, media, (GCompareFunc) compare_media);
1760   if (match == NULL)
1761     return NULL;
1762 
1763   return match->data;
1764 }
1765 
1766 GstHLSVariantStream *
gst_hls_master_playlist_get_variant_for_bitrate(GstHLSMasterPlaylist * playlist,GstHLSVariantStream * current_variant,guint bitrate)1767 gst_hls_master_playlist_get_variant_for_bitrate (GstHLSMasterPlaylist *
1768     playlist, GstHLSVariantStream * current_variant, guint bitrate)
1769 {
1770   GstHLSVariantStream *variant = current_variant;
1771   GList *l;
1772 
1773   /* variant lists are sorted low to high, so iterate from highest to lowest */
1774   if (current_variant == NULL || !current_variant->iframe)
1775     l = g_list_last (playlist->variants);
1776   else
1777     l = g_list_last (playlist->iframe_variants);
1778 
1779   while (l != NULL) {
1780     variant = l->data;
1781     if (variant->bandwidth <= bitrate)
1782       break;
1783     l = l->prev;
1784   }
1785 
1786   return variant;
1787 }
1788 
1789 GstHLSVariantStream *
gst_hls_master_playlist_get_matching_variant(GstHLSMasterPlaylist * playlist,GstHLSVariantStream * current_variant)1790 gst_hls_master_playlist_get_matching_variant (GstHLSMasterPlaylist * playlist,
1791     GstHLSVariantStream * current_variant)
1792 {
1793   if (current_variant->iframe) {
1794     return find_variant_stream_by_uri (playlist->iframe_variants,
1795         current_variant->uri);
1796   }
1797 
1798   return find_variant_stream_by_uri (playlist->variants, current_variant->uri);
1799 }
1800