/* GStreamer * Copyright (C) 2010 Marc-Andre Lureau * * m3u8.c: * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #include #include #include #include "gstfragmented.h" #include "m3u8.h" #define GST_CAT_DEFAULT fragmented_debug static GstM3U8 *gst_m3u8_new (void); static void gst_m3u8_free (GstM3U8 * m3u8); static gboolean gst_m3u8_update (GstM3U8 * m3u8, gchar * data, gboolean * updated); static GstM3U8MediaFile *gst_m3u8_media_file_new (gchar * uri, gchar * title, gint duration, guint sequence); static void gst_m3u8_media_file_free (GstM3U8MediaFile * self); static GstM3U8 * gst_m3u8_new (void) { GstM3U8 *m3u8; m3u8 = g_new0 (GstM3U8, 1); return m3u8; } static void gst_m3u8_set_uri (GstM3U8 * self, gchar * uri) { g_return_if_fail (self != NULL); if (self->uri) g_free (self->uri); self->uri = uri; } static void gst_m3u8_free (GstM3U8 * self) { g_return_if_fail (self != NULL); g_free (self->uri); g_free (self->allowcache); g_free (self->codecs); g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_free, NULL); g_list_free (self->files); g_free (self->last_data); g_list_foreach (self->lists, (GFunc) gst_m3u8_free, NULL); g_list_free (self->lists); g_free (self); } static GstM3U8MediaFile * gst_m3u8_media_file_new (gchar * uri, gchar * title, gint duration, guint sequence) { GstM3U8MediaFile *file; file = g_new0 (GstM3U8MediaFile, 1); file->uri = uri; file->title = title; file->duration = duration; file->sequence = sequence; return file; } static void gst_m3u8_media_file_free (GstM3U8MediaFile * self) { g_return_if_fail (self != NULL); g_free (self->title); g_free (self->uri); g_free (self); } static gboolean int_from_string (gchar * ptr, gchar ** endptr, gint * val) { gchar *end; glong ret; g_return_val_if_fail (ptr != NULL, FALSE); g_return_val_if_fail (val != NULL, FALSE); errno = 0; ret = strtol (ptr, &end, 10); if ((errno == ERANGE && (ret == LONG_MAX || ret == LONG_MIN)) || (errno != 0 && ret == 0)) { GST_WARNING ("%s", g_strerror (errno)); return FALSE; } if (ret > G_MAXINT) { GST_WARNING ("%s", g_strerror (ERANGE)); return FALSE; } if (endptr) *endptr = end; *val = (gint) ret; return end != ptr; } static gboolean parse_attributes (gchar ** ptr, gchar ** a, gchar ** v) { gchar *end, *p; g_return_val_if_fail (ptr != NULL, FALSE); g_return_val_if_fail (*ptr != NULL, FALSE); g_return_val_if_fail (a != NULL, FALSE); g_return_val_if_fail (v != NULL, FALSE); /* [attribute=value,]* */ *a = *ptr; end = p = g_utf8_strchr (*ptr, -1, ','); if (end) { do { end = g_utf8_next_char (end); } while (end && *end == ' '); *p = '\0'; } *v = p = g_utf8_strchr (*ptr, -1, '='); if (*v) { *v = g_utf8_next_char (*v); *p = '\0'; } else { GST_WARNING ("missing = after attribute"); return FALSE; } *ptr = end; return TRUE; } static gint _m3u8_compare_uri (GstM3U8 * a, gchar * uri) { g_return_val_if_fail (a != NULL, 0); g_return_val_if_fail (uri != NULL, 0); return g_strcmp0 (a->uri, uri); } static gint gst_m3u8_compare_playlist_by_bitrate (gconstpointer a, gconstpointer b) { return ((GstM3U8 *) (a))->bandwidth - ((GstM3U8 *) (b))->bandwidth; } /* * @data: a m3u8 playlist text data, taking ownership */ static gboolean gst_m3u8_update (GstM3U8 * self, gchar * data, gboolean * updated) { gint val, duration; gchar *title, *end; // gboolean discontinuity; GstM3U8 *list; g_return_val_if_fail (self != NULL, FALSE); g_return_val_if_fail (data != NULL, FALSE); g_return_val_if_fail (updated != NULL, FALSE); *updated = TRUE; /* check if the data changed since last update */ if (self->last_data && g_str_equal (self->last_data, data)) { GST_DEBUG ("Playlist is the same as previous one"); *updated = FALSE; g_free (data); return TRUE; } if (!g_str_has_prefix (data, "#EXTM3U")) { GST_WARNING ("Data doesn't start with #EXTM3U"); g_free (data); return FALSE; } g_free (self->last_data); self->last_data = data; if (self->files) { g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_free, NULL); g_list_free (self->files); self->files = NULL; } list = NULL; duration = -1; title = NULL; data += 7; while (TRUE) { end = g_utf8_strchr (data, -1, '\n'); /* FIXME: support \r\n */ if (end) *end = '\0'; if (data[0] != '#') { if (duration < 0 && list == NULL) { GST_LOG ("%s: got line without EXTINF or EXTSTREAMINF, dropping", data); goto next_line; } if (!gst_uri_is_valid (data)) { gchar *slash; if (!self->uri) { GST_WARNING ("uri not set, can't build a valid uri"); goto next_line; } slash = g_utf8_strrchr (self->uri, -1, '/'); if (!slash) { GST_WARNING ("Can't build a valid uri"); goto next_line; } *slash = '\0'; data = g_strdup_printf ("%s/%s", self->uri, data); *slash = '/'; } else data = g_strdup (data); if (list != NULL) { if (g_list_find_custom (self->lists, data, (GCompareFunc) _m3u8_compare_uri)) { GST_DEBUG ("Already have a list with this URI"); gst_m3u8_free (list); g_free (data); } else { gst_m3u8_set_uri (list, data); self->lists = g_list_append (self->lists, list); } list = NULL; } else { GstM3U8MediaFile *file; file = gst_m3u8_media_file_new (data, title, duration, self->mediasequence++); duration = -1; title = NULL; self->files = g_list_append (self->files, file); } } else if (g_str_has_prefix (data, "#EXT-X-ENDLIST")) { self->endlist = TRUE; } else if (g_str_has_prefix (data, "#EXT-X-VERSION:")) { if (int_from_string (data + 15, &data, &val)) self->version = val; } else if (g_str_has_prefix (data, "#EXT-X-STREAM-INF:")) { gchar *v, *a; if (list != NULL) { GST_WARNING ("Found a list without a uri..., dropping"); gst_m3u8_free (list); } list = gst_m3u8_new (); data = data + 18; while (data && parse_attributes (&data, &a, &v)) { if (g_str_equal (a, "BANDWIDTH")) { if (!int_from_string (v, NULL, &list->bandwidth)) GST_WARNING ("Error while reading BANDWIDTH"); } else if (g_str_equal (a, "PROGRAM-ID")) { if (!int_from_string (v, NULL, &list->program_id)) GST_WARNING ("Error while reading PROGRAM-ID"); } else if (g_str_equal (a, "CODECS")) { g_free (list->codecs); list->codecs = g_strdup (v); } else if (g_str_equal (a, "RESOLUTION")) { if (!int_from_string (v, &v, &list->width)) GST_WARNING ("Error while reading RESOLUTION width"); if (!v || *v != '=') { GST_WARNING ("Missing height"); } else { v = g_utf8_next_char (v); if (!int_from_string (v, NULL, &list->height)) GST_WARNING ("Error while reading RESOLUTION height"); } } } } else if (g_str_has_prefix (data, "#EXT-X-TARGETDURATION:")) { if (int_from_string (data + 22, &data, &val)) self->targetduration = val; } else if (g_str_has_prefix (data, "#EXT-X-MEDIA-SEQUENCE:")) { if (int_from_string (data + 22, &data, &val)) self->mediasequence = val; } else if (g_str_has_prefix (data, "#EXT-X-DISCONTINUITY")) { /* discontinuity = TRUE; */ } else if (g_str_has_prefix (data, "#EXT-X-PROGRAM-DATE-TIME:")) { /* */ GST_DEBUG ("FIXME parse date"); } else if (g_str_has_prefix (data, "#EXT-X-ALLOW-CACHE:")) { g_free (self->allowcache); self->allowcache = g_strdup (data + 19); } else if (g_str_has_prefix (data, "#EXTINF:")) { if (!int_from_string (data + 8, &data, &val)) { GST_WARNING ("Can't read EXTINF duration"); goto next_line; } duration = val; if (duration > self->targetduration) GST_WARNING ("EXTINF duration > TARGETDURATION"); if (!data || *data != ',') goto next_line; data = g_utf8_next_char (data); if (data != end) { g_free (title); title = g_strdup (data); } } else { GST_LOG ("Ignored line: %s", data); } next_line: if (!end) break; data = g_utf8_next_char (end); /* skip \n */ } /* redorder playlists by bitrate */ if (self->lists) self->lists = g_list_sort (self->lists, (GCompareFunc) gst_m3u8_compare_playlist_by_bitrate); return TRUE; } GstM3U8Client * gst_m3u8_client_new (const gchar * uri) { GstM3U8Client *client; g_return_val_if_fail (uri != NULL, NULL); client = g_new0 (GstM3U8Client, 1); client->main = gst_m3u8_new (); client->current = NULL; client->sequence = -1; client->update_failed_count = 0; gst_m3u8_set_uri (client->main, g_strdup (uri)); return client; } void gst_m3u8_client_free (GstM3U8Client * self) { g_return_if_fail (self != NULL); gst_m3u8_free (self->main); g_free (self); } void gst_m3u8_client_set_current (GstM3U8Client * self, GstM3U8 * m3u8) { g_return_if_fail (self != NULL); if (m3u8 != self->current) { self->current = m3u8; self->update_failed_count = 0; } } gboolean gst_m3u8_client_update (GstM3U8Client * self, gchar * data) { GstM3U8 *m3u8; gboolean updated = FALSE; g_return_val_if_fail (self != NULL, FALSE); m3u8 = self->current ? self->current : self->main; if (!gst_m3u8_update (m3u8, data, &updated)) return FALSE; if (!updated) { self->update_failed_count++; return FALSE; } /* select the first playlist, for now */ if (!self->current) { if (self->main->lists) { self->current = g_list_first (self->main->lists)->data; } else { self->current = self->main; } } if (m3u8->files && self->sequence == -1) { self->sequence = GST_M3U8_MEDIA_FILE (g_list_first (m3u8->files)->data)->sequence; GST_DEBUG ("Setting first sequence at %d", self->sequence); } return TRUE; } static gboolean _find_next (GstM3U8MediaFile * file, GstM3U8Client * client) { GST_DEBUG ("Found fragment %d", file->sequence); if (file->sequence >= client->sequence) return FALSE; return TRUE; } gboolean gst_m3u8_client_get_next_fragment (GstM3U8Client * client, gboolean * discontinuity, const gchar ** uri, GstClockTime * duration, GstClockTime * timestamp) { GList *l; GList *walk; GstM3U8MediaFile *file; g_return_val_if_fail (client != NULL, FALSE); g_return_val_if_fail (client->current != NULL, FALSE); g_return_val_if_fail (discontinuity != NULL, FALSE); GST_DEBUG ("Looking for fragment %d", client->sequence); l = g_list_find_custom (client->current->files, client, (GCompareFunc) _find_next); if (l == NULL) return FALSE; file = GST_M3U8_MEDIA_FILE (l->data); *discontinuity = client->sequence != file->sequence; client->sequence = file->sequence + 1; *uri = file->uri; *duration = file->duration * GST_SECOND; *timestamp = 0; for (walk = client->current->files; walk; walk = walk->next) { if (walk == l) break; *timestamp += GST_M3U8_MEDIA_FILE (walk->data)->duration; } *timestamp *= GST_SECOND; return TRUE; } static void _sum_duration (GstM3U8MediaFile * self, GstClockTime * duration) { *duration += self->duration; } GstClockTime gst_m3u8_client_get_duration (GstM3U8Client * client) { GstClockTime duration = 0; g_return_val_if_fail (client != NULL, GST_CLOCK_TIME_NONE); /* We can only get the duration for on-demand streams */ if (!client->current->endlist) return GST_CLOCK_TIME_NONE; g_list_foreach (client->current->files, (GFunc) _sum_duration, &duration); return duration * GST_SECOND; } const gchar * gst_m3u8_client_get_uri (GstM3U8Client * client) { g_return_val_if_fail (client != NULL, NULL); return client->main->uri; } gboolean gst_m3u8_client_has_variant_playlist (GstM3U8Client * client) { g_return_val_if_fail (client != NULL, FALSE); return client->main->lists != NULL; } gboolean gst_m3u8_client_is_live (GstM3U8Client * client) { g_return_val_if_fail (client != NULL, FALSE); if (!client->current || client->current->endlist) return FALSE; return TRUE; }