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