Imported Upstream version 0.10.23
[profile/ivi/gst-plugins-bad.git] / gst / hls / m3u8.c
1 /* GStreamer
2  * Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
3  *
4  * m3u8.c:
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public
17  * License along with this library; if not, write to the
18  * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19  * Boston, MA 02111-1307, USA.
20  */
21
22 #include <stdlib.h>
23 #include <errno.h>
24 #include <glib.h>
25
26 #include "gstfragmented.h"
27 #include "m3u8.h"
28
29 #define GST_CAT_DEFAULT fragmented_debug
30
31 static GstM3U8 *gst_m3u8_new (void);
32 static void gst_m3u8_free (GstM3U8 * m3u8);
33 static gboolean gst_m3u8_update (GstM3U8 * m3u8, gchar * data,
34     gboolean * updated);
35 static GstM3U8MediaFile *gst_m3u8_media_file_new (gchar * uri,
36     gchar * title, gint duration, guint sequence);
37 static void gst_m3u8_media_file_free (GstM3U8MediaFile * self);
38
39 static GstM3U8 *
40 gst_m3u8_new (void)
41 {
42   GstM3U8 *m3u8;
43
44   m3u8 = g_new0 (GstM3U8, 1);
45
46   return m3u8;
47 }
48
49 static void
50 gst_m3u8_set_uri (GstM3U8 * self, gchar * uri)
51 {
52   g_return_if_fail (self != NULL);
53
54   if (self->uri)
55     g_free (self->uri);
56   self->uri = uri;
57 }
58
59 static void
60 gst_m3u8_free (GstM3U8 * self)
61 {
62   g_return_if_fail (self != NULL);
63
64   g_free (self->uri);
65   g_free (self->allowcache);
66   g_free (self->codecs);
67
68   g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_free, NULL);
69   g_list_free (self->files);
70
71   g_free (self->last_data);
72   g_list_foreach (self->lists, (GFunc) gst_m3u8_free, NULL);
73   g_list_free (self->lists);
74
75   g_free (self);
76 }
77
78 static GstM3U8MediaFile *
79 gst_m3u8_media_file_new (gchar * uri, gchar * title, gint duration,
80     guint sequence)
81 {
82   GstM3U8MediaFile *file;
83
84   file = g_new0 (GstM3U8MediaFile, 1);
85   file->uri = uri;
86   file->title = title;
87   file->duration = duration;
88   file->sequence = sequence;
89
90   return file;
91 }
92
93 static void
94 gst_m3u8_media_file_free (GstM3U8MediaFile * self)
95 {
96   g_return_if_fail (self != NULL);
97
98   g_free (self->title);
99   g_free (self->uri);
100   g_free (self);
101 }
102
103 static gboolean
104 int_from_string (gchar * ptr, gchar ** endptr, gint * val)
105 {
106   gchar *end;
107   glong ret;
108
109   g_return_val_if_fail (ptr != NULL, FALSE);
110   g_return_val_if_fail (val != NULL, FALSE);
111
112   errno = 0;
113   ret = strtol (ptr, &end, 10);
114   if ((errno == ERANGE && (ret == LONG_MAX || ret == LONG_MIN))
115       || (errno != 0 && ret == 0)) {
116     GST_WARNING ("%s", g_strerror (errno));
117     return FALSE;
118   }
119
120   if (ret > G_MAXINT) {
121     GST_WARNING ("%s", g_strerror (ERANGE));
122     return FALSE;
123   }
124
125   if (endptr)
126     *endptr = end;
127
128   *val = (gint) ret;
129
130   return end != ptr;
131 }
132
133 static gboolean
134 parse_attributes (gchar ** ptr, gchar ** a, gchar ** v)
135 {
136   gchar *end, *p;
137
138   g_return_val_if_fail (ptr != NULL, FALSE);
139   g_return_val_if_fail (*ptr != NULL, FALSE);
140   g_return_val_if_fail (a != NULL, FALSE);
141   g_return_val_if_fail (v != NULL, FALSE);
142
143   /* [attribute=value,]* */
144
145   *a = *ptr;
146   end = p = g_utf8_strchr (*ptr, -1, ',');
147   if (end) {
148     do {
149       end = g_utf8_next_char (end);
150     } while (end && *end == ' ');
151     *p = '\0';
152   }
153
154   *v = p = g_utf8_strchr (*ptr, -1, '=');
155   if (*v) {
156     *v = g_utf8_next_char (*v);
157     *p = '\0';
158   } else {
159     GST_WARNING ("missing = after attribute");
160     return FALSE;
161   }
162
163   *ptr = end;
164   return TRUE;
165 }
166
167 static gint
168 _m3u8_compare_uri (GstM3U8 * a, gchar * uri)
169 {
170   g_return_val_if_fail (a != NULL, 0);
171   g_return_val_if_fail (uri != NULL, 0);
172
173   return g_strcmp0 (a->uri, uri);
174 }
175
176 static gint
177 gst_m3u8_compare_playlist_by_bitrate (gconstpointer a, gconstpointer b)
178 {
179   return ((GstM3U8 *) (a))->bandwidth - ((GstM3U8 *) (b))->bandwidth;
180 }
181
182 /*
183  * @data: a m3u8 playlist text data, taking ownership
184  */
185 static gboolean
186 gst_m3u8_update (GstM3U8 * self, gchar * data, gboolean * updated)
187 {
188   gint val, duration;
189   gchar *title, *end;
190 //  gboolean discontinuity;
191   GstM3U8 *list;
192
193   g_return_val_if_fail (self != NULL, FALSE);
194   g_return_val_if_fail (data != NULL, FALSE);
195   g_return_val_if_fail (updated != NULL, FALSE);
196
197   *updated = TRUE;
198
199   /* check if the data changed since last update */
200   if (self->last_data && g_str_equal (self->last_data, data)) {
201     GST_DEBUG ("Playlist is the same as previous one");
202     *updated = FALSE;
203     g_free (data);
204     return TRUE;
205   }
206
207   if (!g_str_has_prefix (data, "#EXTM3U")) {
208     GST_WARNING ("Data doesn't start with #EXTM3U");
209     *updated = FALSE;
210     g_free (data);
211     return FALSE;
212   }
213
214   g_free (self->last_data);
215   self->last_data = data;
216
217   if (self->files) {
218     g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_free, NULL);
219     g_list_free (self->files);
220     self->files = NULL;
221   }
222
223   list = NULL;
224   duration = -1;
225   title = NULL;
226   data += 7;
227   while (TRUE) {
228     end = g_utf8_strchr (data, -1, '\n');
229     if (end)
230       *end = '\0';
231
232     if (data[0] != '#') {
233       gchar *r;
234
235       if (duration < 0 && list == NULL) {
236         GST_LOG ("%s: got line without EXTINF or EXTSTREAMINF, dropping", data);
237         goto next_line;
238       }
239
240       if (!gst_uri_is_valid (data)) {
241         gchar *slash;
242         if (!self->uri) {
243           GST_WARNING ("uri not set, can't build a valid uri");
244           goto next_line;
245         }
246         slash = g_utf8_strrchr (self->uri, -1, '/');
247         if (!slash) {
248           GST_WARNING ("Can't build a valid uri");
249           goto next_line;
250         }
251
252         *slash = '\0';
253         data = g_strdup_printf ("%s/%s", self->uri, data);
254         *slash = '/';
255       } else {
256         data = g_strdup (data);
257       }
258
259       r = g_utf8_strchr (data, -1, '\r');
260       if (r)
261         *r = '\0';
262
263       if (list != NULL) {
264         if (g_list_find_custom (self->lists, data,
265                 (GCompareFunc) _m3u8_compare_uri)) {
266           GST_DEBUG ("Already have a list with this URI");
267           gst_m3u8_free (list);
268           g_free (data);
269         } else {
270           gst_m3u8_set_uri (list, data);
271           self->lists = g_list_append (self->lists, list);
272         }
273         list = NULL;
274       } else {
275         GstM3U8MediaFile *file;
276         file =
277             gst_m3u8_media_file_new (data, title, duration,
278             self->mediasequence++);
279         duration = -1;
280         title = NULL;
281         self->files = g_list_append (self->files, file);
282       }
283
284     } else if (g_str_has_prefix (data, "#EXT-X-ENDLIST")) {
285       self->endlist = TRUE;
286     } else if (g_str_has_prefix (data, "#EXT-X-VERSION:")) {
287       if (int_from_string (data + 15, &data, &val))
288         self->version = val;
289     } else if (g_str_has_prefix (data, "#EXT-X-STREAM-INF:")) {
290       gchar *v, *a;
291
292       if (list != NULL) {
293         GST_WARNING ("Found a list without a uri..., dropping");
294         gst_m3u8_free (list);
295       }
296
297       list = gst_m3u8_new ();
298       data = data + 18;
299       while (data && parse_attributes (&data, &a, &v)) {
300         if (g_str_equal (a, "BANDWIDTH")) {
301           if (!int_from_string (v, NULL, &list->bandwidth))
302             GST_WARNING ("Error while reading BANDWIDTH");
303         } else if (g_str_equal (a, "PROGRAM-ID")) {
304           if (!int_from_string (v, NULL, &list->program_id))
305             GST_WARNING ("Error while reading PROGRAM-ID");
306         } else if (g_str_equal (a, "CODECS")) {
307           g_free (list->codecs);
308           list->codecs = g_strdup (v);
309         } else if (g_str_equal (a, "RESOLUTION")) {
310           if (!int_from_string (v, &v, &list->width))
311             GST_WARNING ("Error while reading RESOLUTION width");
312           if (!v || *v != '=') {
313             GST_WARNING ("Missing height");
314           } else {
315             v = g_utf8_next_char (v);
316             if (!int_from_string (v, NULL, &list->height))
317               GST_WARNING ("Error while reading RESOLUTION height");
318           }
319         }
320       }
321     } else if (g_str_has_prefix (data, "#EXT-X-TARGETDURATION:")) {
322       if (int_from_string (data + 22, &data, &val))
323         self->targetduration = val;
324     } else if (g_str_has_prefix (data, "#EXT-X-MEDIA-SEQUENCE:")) {
325       if (int_from_string (data + 22, &data, &val))
326         self->mediasequence = val;
327     } else if (g_str_has_prefix (data, "#EXT-X-DISCONTINUITY")) {
328       /* discontinuity = TRUE; */
329     } else if (g_str_has_prefix (data, "#EXT-X-PROGRAM-DATE-TIME:")) {
330       /* <YYYY-MM-DDThh:mm:ssZ> */
331       GST_DEBUG ("FIXME parse date");
332     } else if (g_str_has_prefix (data, "#EXT-X-ALLOW-CACHE:")) {
333       g_free (self->allowcache);
334       self->allowcache = g_strdup (data + 19);
335     } else if (g_str_has_prefix (data, "#EXTINF:")) {
336       if (!int_from_string (data + 8, &data, &val)) {
337         GST_WARNING ("Can't read EXTINF duration");
338         goto next_line;
339       }
340       duration = val;
341       if (duration > self->targetduration)
342         GST_WARNING ("EXTINF duration > TARGETDURATION");
343       if (!data || *data != ',')
344         goto next_line;
345       data = g_utf8_next_char (data);
346       if (data != end) {
347         g_free (title);
348         title = g_strdup (data);
349       }
350     } else {
351       GST_LOG ("Ignored line: %s", data);
352     }
353
354   next_line:
355     if (!end)
356       break;
357     data = g_utf8_next_char (end);      /* skip \n */
358   }
359
360   /* redorder playlists by bitrate */
361   if (self->lists) {
362     gchar *top_variant_uri = NULL;
363
364     if (!self->current_variant)
365       top_variant_uri = GST_M3U8 (self->lists->data)->uri;
366     else
367       top_variant_uri = GST_M3U8 (self->current_variant->data)->uri;
368
369     self->lists =
370         g_list_sort (self->lists,
371         (GCompareFunc) gst_m3u8_compare_playlist_by_bitrate);
372
373     self->current_variant = g_list_find_custom (self->lists, top_variant_uri,
374         (GCompareFunc) _m3u8_compare_uri);
375   }
376
377   return TRUE;
378 }
379
380 GstM3U8Client *
381 gst_m3u8_client_new (const gchar * uri)
382 {
383   GstM3U8Client *client;
384
385   g_return_val_if_fail (uri != NULL, NULL);
386
387   client = g_new0 (GstM3U8Client, 1);
388   client->main = gst_m3u8_new ();
389   client->current = NULL;
390   client->sequence = -1;
391   client->update_failed_count = 0;
392   client->lock = g_mutex_new ();
393   gst_m3u8_set_uri (client->main, g_strdup (uri));
394
395   return client;
396 }
397
398 void
399 gst_m3u8_client_free (GstM3U8Client * self)
400 {
401   g_return_if_fail (self != NULL);
402
403   gst_m3u8_free (self->main);
404   g_mutex_free (self->lock);
405   g_free (self);
406 }
407
408 void
409 gst_m3u8_client_set_current (GstM3U8Client * self, GstM3U8 * m3u8)
410 {
411   g_return_if_fail (self != NULL);
412
413   GST_M3U8_CLIENT_LOCK (self);
414   if (m3u8 != self->current) {
415     self->current = m3u8;
416     self->update_failed_count = 0;
417   }
418   GST_M3U8_CLIENT_UNLOCK (self);
419 }
420
421 gboolean
422 gst_m3u8_client_update (GstM3U8Client * self, gchar * data)
423 {
424   GstM3U8 *m3u8;
425   gboolean updated = FALSE;
426   gboolean ret = FALSE;
427
428   g_return_val_if_fail (self != NULL, FALSE);
429
430   GST_M3U8_CLIENT_LOCK (self);
431   m3u8 = self->current ? self->current : self->main;
432
433   if (!gst_m3u8_update (m3u8, data, &updated))
434     goto out;
435
436   if (!updated) {
437     self->update_failed_count++;
438     goto out;
439   }
440
441   /* select the first playlist, for now */
442   if (!self->current) {
443     if (self->main->lists) {
444       self->current = self->main->current_variant->data;
445     } else {
446       self->current = self->main;
447     }
448   }
449
450   if (m3u8->files && self->sequence == -1) {
451     self->sequence =
452         GST_M3U8_MEDIA_FILE (g_list_first (m3u8->files)->data)->sequence;
453     GST_DEBUG ("Setting first sequence at %d", self->sequence);
454   }
455
456   ret = TRUE;
457 out:
458   GST_M3U8_CLIENT_UNLOCK (self);
459   return ret;
460 }
461
462 static gboolean
463 _find_next (GstM3U8MediaFile * file, GstM3U8Client * client)
464 {
465   GST_DEBUG ("Found fragment %d", file->sequence);
466   if (file->sequence >= client->sequence)
467     return FALSE;
468   return TRUE;
469 }
470
471 void
472 gst_m3u8_client_get_current_position (GstM3U8Client * client,
473     GstClockTime * timestamp)
474 {
475   GList *l;
476   GList *walk;
477
478   l = g_list_find_custom (client->current->files, client,
479       (GCompareFunc) _find_next);
480
481   *timestamp = 0;
482   for (walk = client->current->files; walk; walk = walk->next) {
483     if (walk == l)
484       break;
485     *timestamp += GST_M3U8_MEDIA_FILE (walk->data)->duration;
486   }
487   *timestamp *= GST_SECOND;
488 }
489
490 gboolean
491 gst_m3u8_client_get_next_fragment (GstM3U8Client * client,
492     gboolean * discontinuity, const gchar ** uri, GstClockTime * duration,
493     GstClockTime * timestamp)
494 {
495   GList *l;
496   GstM3U8MediaFile *file;
497
498   g_return_val_if_fail (client != NULL, FALSE);
499   g_return_val_if_fail (client->current != NULL, FALSE);
500   g_return_val_if_fail (discontinuity != NULL, FALSE);
501
502   GST_M3U8_CLIENT_LOCK (client);
503   GST_DEBUG ("Looking for fragment %d", client->sequence);
504   l = g_list_find_custom (client->current->files, client,
505       (GCompareFunc) _find_next);
506   if (l == NULL) {
507     GST_M3U8_CLIENT_UNLOCK (client);
508     return FALSE;
509   }
510
511   gst_m3u8_client_get_current_position (client, timestamp);
512
513   file = GST_M3U8_MEDIA_FILE (l->data);
514
515   *discontinuity = client->sequence != file->sequence;
516   client->sequence = file->sequence + 1;
517
518   *uri = file->uri;
519   *duration = file->duration * GST_SECOND;
520
521   GST_M3U8_CLIENT_UNLOCK (client);
522   return TRUE;
523 }
524
525 static void
526 _sum_duration (GstM3U8MediaFile * self, GstClockTime * duration)
527 {
528   *duration += self->duration;
529 }
530
531 GstClockTime
532 gst_m3u8_client_get_duration (GstM3U8Client * client)
533 {
534   GstClockTime duration = 0;
535
536   g_return_val_if_fail (client != NULL, GST_CLOCK_TIME_NONE);
537
538   GST_M3U8_CLIENT_LOCK (client);
539   /* We can only get the duration for on-demand streams */
540   if (!client->current->endlist) {
541     GST_M3U8_CLIENT_UNLOCK (client);
542     return GST_CLOCK_TIME_NONE;
543   }
544
545   g_list_foreach (client->current->files, (GFunc) _sum_duration, &duration);
546   GST_M3U8_CLIENT_UNLOCK (client);
547   return duration * GST_SECOND;
548 }
549
550 GstClockTime
551 gst_m3u8_client_get_target_duration (GstM3U8Client * client)
552 {
553   GstClockTime duration = 0;
554
555   g_return_val_if_fail (client != NULL, GST_CLOCK_TIME_NONE);
556
557   GST_M3U8_CLIENT_LOCK (client);
558   duration = client->current->targetduration;
559   GST_M3U8_CLIENT_UNLOCK (client);
560   return duration * GST_SECOND;
561 }
562
563 const gchar *
564 gst_m3u8_client_get_uri (GstM3U8Client * client)
565 {
566   const gchar *uri;
567
568   g_return_val_if_fail (client != NULL, NULL);
569
570   GST_M3U8_CLIENT_LOCK (client);
571   uri = client->main->uri;
572   GST_M3U8_CLIENT_UNLOCK (client);
573   return uri;
574 }
575
576 const gchar *
577 gst_m3u8_client_get_current_uri (GstM3U8Client * client)
578 {
579   const gchar *uri;
580
581   g_return_val_if_fail (client != NULL, NULL);
582
583   GST_M3U8_CLIENT_LOCK (client);
584   uri = client->current->uri;
585   GST_M3U8_CLIENT_UNLOCK (client);
586   return uri;
587 }
588
589 gboolean
590 gst_m3u8_client_has_variant_playlist (GstM3U8Client * client)
591 {
592   gboolean ret;
593
594   g_return_val_if_fail (client != NULL, FALSE);
595
596   GST_M3U8_CLIENT_LOCK (client);
597   ret = (client->main->lists != NULL);
598   GST_M3U8_CLIENT_UNLOCK (client);
599   return ret;
600 }
601
602 gboolean
603 gst_m3u8_client_is_live (GstM3U8Client * client)
604 {
605   gboolean ret;
606
607   g_return_val_if_fail (client != NULL, FALSE);
608
609   GST_M3U8_CLIENT_LOCK (client);
610   if (!client->current || client->current->endlist)
611     ret = FALSE;
612   else
613     ret = TRUE;
614   GST_M3U8_CLIENT_UNLOCK (client);
615   return ret;
616 }