gstreamer/gst/hls/m3u8.c
Youness Alaoui 690e59e497 hlsdemux: Keep a separate variable for the current variant used
By using a separate variable, first it allows us to sort the lists
of alternates but keep the pointer on the first occurence in the main
playlist (to respect the spec of starting with the bitrate specified
first in the main playlist). It also avoid playing with the lists variable
which should be used to store the list of playlists and not as a pointer
to the current one.
Also fixes a memleak with the g_list_foreach freeing the lists, if it wasn't
pointing to the first element of the list.
2011-08-22 15:46:41 +02:00

539 lines
13 KiB
C

/* GStreamer
* Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
*
* 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 <stdlib.h>
#include <errno.h>
#include <glib.h>
#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");
*updated = FALSE;
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:")) {
/* <YYYY-MM-DDThh:mm:ssZ> */
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) {
gchar *top_variant_uri = NULL;
if (!self->current_variant)
top_variant_uri = GST_M3U8 (self->lists->data)->uri;
else
top_variant_uri = GST_M3U8 (self->current_variant->data)->uri;
self->lists =
g_list_sort (self->lists,
(GCompareFunc) gst_m3u8_compare_playlist_by_bitrate);
self->current_variant = g_list_find_custom (self->lists, top_variant_uri,
(GCompareFunc) _m3u8_compare_uri);
}
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 = self->main->current_variant->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;
}