mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-09 10:59:39 +00:00
f0fcf1d718
Make M3U8 and GstM3U8MediaFile refcounted. The contents of it and GstM3U8MediaFile are pretty much immutable already, but if we make it refcounted we can just return a ref to the media file from _get_next_fragment() instead of copying over all fields one-by-one, and then copying them all into the adaptive stream structure fields again. Move state from client into m3u8 structure. This will be useful later when we'll have multiple media playlists being streamed at the same time, as will be the case with alternative renditions. This has the downside that we need to copy over some state when we switch between variant streams. The GstM3U8Client structure is gone, and main/current lists are not directly in hlsdemux. hlsdemux had as many CLIENT_LOCK/UNLOCK as the m3u8 code anyway...
1240 lines
32 KiB
C
1240 lines
32 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., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include <errno.h>
|
|
#include <glib.h>
|
|
#include <string.h>
|
|
|
|
#include "gsthls.h"
|
|
#include "m3u8.h"
|
|
|
|
#define GST_CAT_DEFAULT hls_debug
|
|
|
|
static GstM3U8MediaFile *gst_m3u8_media_file_new (gchar * uri,
|
|
gchar * title, GstClockTime duration, guint sequence);
|
|
gchar *uri_join (const gchar * uri, const gchar * path);
|
|
static gboolean gst_m3u8_update_master_playlist (GstM3U8 * self, gchar * data);
|
|
|
|
GstM3U8 *
|
|
gst_m3u8_new (void)
|
|
{
|
|
GstM3U8 *m3u8;
|
|
|
|
m3u8 = g_new0 (GstM3U8, 1);
|
|
|
|
m3u8->current_file = NULL;
|
|
m3u8->current_file_duration = GST_CLOCK_TIME_NONE;
|
|
m3u8->sequence = -1;
|
|
m3u8->sequence_position = 0;
|
|
m3u8->highest_sequence_number = -1;
|
|
m3u8->duration = GST_CLOCK_TIME_NONE;
|
|
|
|
g_mutex_init (&m3u8->lock);
|
|
m3u8->ref_count = 1;
|
|
|
|
return m3u8;
|
|
}
|
|
|
|
/* call with M3U8_LOCK held */
|
|
static void
|
|
gst_m3u8_take_uri (GstM3U8 * self, gchar * uri, gchar * base_uri, gchar * name)
|
|
{
|
|
g_return_if_fail (self != NULL);
|
|
|
|
if (self->uri != uri) {
|
|
g_free (self->uri);
|
|
self->uri = uri;
|
|
}
|
|
if (self->base_uri != base_uri) {
|
|
g_free (self->base_uri);
|
|
self->base_uri = base_uri;
|
|
}
|
|
if (self->name != name) {
|
|
g_free (self->name);
|
|
self->name = name;
|
|
}
|
|
}
|
|
|
|
void
|
|
gst_m3u8_set_uri (GstM3U8 * m3u8, const gchar * uri, const gchar * base_uri,
|
|
const gchar * name)
|
|
{
|
|
GST_M3U8_LOCK (m3u8);
|
|
gst_m3u8_take_uri (m3u8, g_strdup (uri), g_strdup (base_uri),
|
|
g_strdup (name));
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
}
|
|
|
|
GstM3U8 *
|
|
gst_m3u8_ref (GstM3U8 * m3u8)
|
|
{
|
|
g_assert (m3u8 != NULL && m3u8->ref_count > 0);
|
|
|
|
g_atomic_int_add (&m3u8->ref_count, 1);
|
|
return m3u8;
|
|
}
|
|
|
|
void
|
|
gst_m3u8_unref (GstM3U8 * self)
|
|
{
|
|
g_return_if_fail (self != NULL && self->ref_count > 0);
|
|
|
|
if (g_atomic_int_dec_and_test (&self->ref_count)) {
|
|
g_free (self->uri);
|
|
g_free (self->base_uri);
|
|
g_free (self->name);
|
|
g_free (self->codecs);
|
|
|
|
g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_unref, NULL);
|
|
g_list_free (self->files);
|
|
|
|
g_free (self->last_data);
|
|
g_list_foreach (self->lists, (GFunc) gst_m3u8_unref, NULL);
|
|
g_list_free (self->lists);
|
|
g_list_foreach (self->iframe_lists, (GFunc) gst_m3u8_unref, NULL);
|
|
g_list_free (self->iframe_lists);
|
|
|
|
g_free (self);
|
|
}
|
|
}
|
|
|
|
static GstM3U8MediaFile *
|
|
gst_m3u8_media_file_new (gchar * uri, gchar * title, GstClockTime duration,
|
|
guint sequence)
|
|
{
|
|
GstM3U8MediaFile *file;
|
|
|
|
file = g_new0 (GstM3U8MediaFile, 1);
|
|
file->uri = uri;
|
|
file->title = title;
|
|
file->duration = duration;
|
|
file->sequence = sequence;
|
|
file->ref_count = 1;
|
|
|
|
return file;
|
|
}
|
|
|
|
GstM3U8MediaFile *
|
|
gst_m3u8_media_file_ref (GstM3U8MediaFile * mfile)
|
|
{
|
|
g_assert (mfile != NULL && mfile->ref_count > 0);
|
|
|
|
g_atomic_int_add (&mfile->ref_count, 1);
|
|
return mfile;
|
|
}
|
|
|
|
void
|
|
gst_m3u8_media_file_unref (GstM3U8MediaFile * self)
|
|
{
|
|
g_return_if_fail (self != NULL && self->ref_count > 0);
|
|
|
|
if (g_atomic_int_dec_and_test (&self->ref_count)) {
|
|
g_free (self->title);
|
|
g_free (self->uri);
|
|
g_free (self->key);
|
|
g_free (self);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
int_from_string (gchar * ptr, gchar ** endptr, gint * val)
|
|
{
|
|
gchar *end;
|
|
gint64 ret;
|
|
|
|
g_return_val_if_fail (ptr != NULL, FALSE);
|
|
g_return_val_if_fail (val != NULL, FALSE);
|
|
|
|
errno = 0;
|
|
ret = g_ascii_strtoll (ptr, &end, 10);
|
|
if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64))
|
|
|| (errno != 0 && ret == 0)) {
|
|
GST_WARNING ("%s", g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
if (ret > G_MAXINT || ret < G_MININT) {
|
|
GST_WARNING ("%s", g_strerror (ERANGE));
|
|
return FALSE;
|
|
}
|
|
|
|
if (endptr)
|
|
*endptr = end;
|
|
|
|
*val = (gint) ret;
|
|
|
|
return end != ptr;
|
|
}
|
|
|
|
static gboolean
|
|
int64_from_string (gchar * ptr, gchar ** endptr, gint64 * val)
|
|
{
|
|
gchar *end;
|
|
gint64 ret;
|
|
|
|
g_return_val_if_fail (ptr != NULL, FALSE);
|
|
g_return_val_if_fail (val != NULL, FALSE);
|
|
|
|
errno = 0;
|
|
ret = g_ascii_strtoll (ptr, &end, 10);
|
|
if ((errno == ERANGE && (ret == G_MAXINT64 || ret == G_MININT64))
|
|
|| (errno != 0 && ret == 0)) {
|
|
GST_WARNING ("%s", g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
if (endptr)
|
|
*endptr = end;
|
|
|
|
*val = ret;
|
|
|
|
return end != ptr;
|
|
}
|
|
|
|
static gboolean
|
|
double_from_string (gchar * ptr, gchar ** endptr, gdouble * val)
|
|
{
|
|
gchar *end;
|
|
gdouble ret;
|
|
|
|
g_return_val_if_fail (ptr != NULL, FALSE);
|
|
g_return_val_if_fail (val != NULL, FALSE);
|
|
|
|
errno = 0;
|
|
ret = g_ascii_strtod (ptr, &end);
|
|
if ((errno == ERANGE && (ret == HUGE_VAL || ret == -HUGE_VAL))
|
|
|| (errno != 0 && ret == 0)) {
|
|
GST_WARNING ("%s", g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
if (!isfinite (ret)) {
|
|
GST_WARNING ("%s", g_strerror (ERANGE));
|
|
return FALSE;
|
|
}
|
|
|
|
if (endptr)
|
|
*endptr = end;
|
|
|
|
*val = (gdouble) ret;
|
|
|
|
return end != ptr;
|
|
}
|
|
|
|
static gboolean
|
|
parse_attributes (gchar ** ptr, gchar ** a, gchar ** v)
|
|
{
|
|
gchar *end = NULL, *p, *ve;
|
|
|
|
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) {
|
|
gchar *q = g_utf8_strchr (*ptr, -1, '"');
|
|
if (q && q < end) {
|
|
/* special case, such as CODECS="avc1.77.30, mp4a.40.2" */
|
|
q = g_utf8_next_char (q);
|
|
if (q) {
|
|
q = g_utf8_strchr (q, -1, '"');
|
|
}
|
|
if (q) {
|
|
end = p = g_utf8_strchr (q, -1, ',');
|
|
}
|
|
}
|
|
}
|
|
if (end) {
|
|
do {
|
|
end = g_utf8_next_char (end);
|
|
} while (end && *end == ' ');
|
|
*p = '\0';
|
|
}
|
|
|
|
*v = p = g_utf8_strchr (*ptr, -1, '=');
|
|
if (*v) {
|
|
*p = '\0';
|
|
*v = g_utf8_next_char (*v);
|
|
if (**v == '"') {
|
|
ve = g_utf8_next_char (*v);
|
|
if (ve) {
|
|
ve = g_utf8_strchr (ve, -1, '"');
|
|
}
|
|
if (ve) {
|
|
*v = g_utf8_next_char (*v);
|
|
*ve = '\0';
|
|
} else {
|
|
GST_WARNING ("Cannot remove quotation marks from %s", *a);
|
|
}
|
|
}
|
|
} 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
|
|
*/
|
|
gboolean
|
|
gst_m3u8_update (GstM3U8 * self, gchar * data)
|
|
{
|
|
gint val;
|
|
GstClockTime duration;
|
|
gchar *title, *end;
|
|
gboolean discontinuity = FALSE;
|
|
gchar *current_key = NULL;
|
|
gboolean have_iv = FALSE;
|
|
guint8 iv[16] = { 0, };
|
|
gint64 size = -1, offset = -1;
|
|
gint64 mediasequence;
|
|
|
|
g_return_val_if_fail (self != NULL, FALSE);
|
|
g_return_val_if_fail (data != NULL, FALSE);
|
|
|
|
GST_M3U8_LOCK (self);
|
|
|
|
/* 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");
|
|
g_free (data);
|
|
GST_M3U8_UNLOCK (self);
|
|
return TRUE;
|
|
}
|
|
|
|
if (!g_str_has_prefix (data, "#EXTM3U")) {
|
|
GST_WARNING ("Data doesn't start with #EXTM3U");
|
|
g_free (data);
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
if (g_strrstr (data, "\n#EXT-X-STREAM-INF:") != NULL) {
|
|
GST_DEBUG ("Not a media playlist, but a master playlist!");
|
|
GST_M3U8_UNLOCK (self);
|
|
return gst_m3u8_update_master_playlist (self, data);
|
|
}
|
|
|
|
GST_TRACE ("data:\n%s", data);
|
|
|
|
g_free (self->last_data);
|
|
self->last_data = data;
|
|
|
|
self->current_file = NULL;
|
|
if (self->files) {
|
|
g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_unref, NULL);
|
|
g_list_free (self->files);
|
|
self->files = NULL;
|
|
}
|
|
self->duration = GST_CLOCK_TIME_NONE;
|
|
mediasequence = 0;
|
|
|
|
/* By default, allow caching */
|
|
self->allowcache = TRUE;
|
|
|
|
duration = 0;
|
|
title = NULL;
|
|
data += 7;
|
|
while (TRUE) {
|
|
gchar *r;
|
|
|
|
end = g_utf8_strchr (data, -1, '\n');
|
|
if (end)
|
|
*end = '\0';
|
|
|
|
r = g_utf8_strchr (data, -1, '\r');
|
|
if (r)
|
|
*r = '\0';
|
|
|
|
if (data[0] != '#' && data[0] != '\0') {
|
|
if (duration <= 0) {
|
|
GST_LOG ("%s: got line without EXTINF, dropping", data);
|
|
goto next_line;
|
|
}
|
|
|
|
data = uri_join (self->base_uri ? self->base_uri : self->uri, data);
|
|
if (data != NULL) {
|
|
GstM3U8MediaFile *file;
|
|
file = gst_m3u8_media_file_new (data, title, duration, mediasequence++);
|
|
|
|
/* set encryption params */
|
|
file->key = current_key ? g_strdup (current_key) : NULL;
|
|
if (file->key) {
|
|
if (have_iv) {
|
|
memcpy (file->iv, iv, sizeof (iv));
|
|
} else {
|
|
guint8 *iv = file->iv + 12;
|
|
GST_WRITE_UINT32_BE (iv, file->sequence);
|
|
}
|
|
}
|
|
|
|
if (size != -1) {
|
|
file->size = size;
|
|
if (offset != -1) {
|
|
file->offset = offset;
|
|
} else {
|
|
GstM3U8MediaFile *prev = self->files ? self->files->data : NULL;
|
|
|
|
if (!prev) {
|
|
offset = 0;
|
|
} else {
|
|
offset = prev->offset + prev->size;
|
|
}
|
|
file->offset = offset;
|
|
}
|
|
} else {
|
|
file->size = -1;
|
|
file->offset = 0;
|
|
}
|
|
|
|
file->discont = discontinuity;
|
|
|
|
duration = 0;
|
|
title = NULL;
|
|
discontinuity = FALSE;
|
|
size = offset = -1;
|
|
self->files = g_list_prepend (self->files, file);
|
|
}
|
|
|
|
} else if (g_str_has_prefix (data, "#EXTINF:")) {
|
|
gdouble fval;
|
|
if (!double_from_string (data + 8, &data, &fval)) {
|
|
GST_WARNING ("Can't read EXTINF duration");
|
|
goto next_line;
|
|
}
|
|
duration = fval * (gdouble) GST_SECOND;
|
|
if (self->targetduration > 0 && duration > self->targetduration) {
|
|
GST_WARNING ("EXTINF duration (%" GST_TIME_FORMAT
|
|
") > TARGETDURATION (%" GST_TIME_FORMAT ")",
|
|
GST_TIME_ARGS (duration), GST_TIME_ARGS (self->targetduration));
|
|
}
|
|
if (!data || *data != ',')
|
|
goto next_line;
|
|
data = g_utf8_next_char (data);
|
|
if (data != end) {
|
|
g_free (title);
|
|
title = g_strdup (data);
|
|
}
|
|
} else if (g_str_has_prefix (data, "#EXT-X-")) {
|
|
gchar *data_ext_x = data + 7;
|
|
|
|
/* All these entries start with #EXT-X- */
|
|
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, "TARGETDURATION:")) {
|
|
if (int_from_string (data + 22, &data, &val))
|
|
self->targetduration = val * GST_SECOND;
|
|
} else if (g_str_has_prefix (data_ext_x, "MEDIA-SEQUENCE:")) {
|
|
if (int_from_string (data + 22, &data, &val))
|
|
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:")) {
|
|
self->allowcache = g_ascii_strcasecmp (data + 19, "YES") == 0;
|
|
} else if (g_str_has_prefix (data_ext_x, "KEY:")) {
|
|
gchar *v, *a;
|
|
|
|
data = data + 11;
|
|
|
|
/* IV and KEY are only valid until the next #EXT-X-KEY */
|
|
have_iv = FALSE;
|
|
g_free (current_key);
|
|
current_key = NULL;
|
|
while (data && parse_attributes (&data, &a, &v)) {
|
|
if (g_str_equal (a, "URI")) {
|
|
current_key =
|
|
uri_join (self->base_uri ? self->base_uri : self->uri, v);
|
|
} else if (g_str_equal (a, "IV")) {
|
|
gchar *ivp = v;
|
|
gint i;
|
|
|
|
if (strlen (ivp) < 32 + 2 || (!g_str_has_prefix (ivp, "0x")
|
|
&& !g_str_has_prefix (ivp, "0X"))) {
|
|
GST_WARNING ("Can't read IV");
|
|
continue;
|
|
}
|
|
|
|
ivp += 2;
|
|
for (i = 0; i < 16; i++) {
|
|
gint h, l;
|
|
|
|
h = g_ascii_xdigit_value (*ivp);
|
|
ivp++;
|
|
l = g_ascii_xdigit_value (*ivp);
|
|
ivp++;
|
|
if (h == -1 || l == -1) {
|
|
i = -1;
|
|
break;
|
|
}
|
|
iv[i] = (h << 4) | l;
|
|
}
|
|
|
|
if (i == -1) {
|
|
GST_WARNING ("Can't read IV");
|
|
continue;
|
|
}
|
|
have_iv = TRUE;
|
|
} else if (g_str_equal (a, "METHOD")) {
|
|
if (!g_str_equal (v, "AES-128")) {
|
|
GST_WARNING ("Encryption method %s not supported", v);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
} else if (g_str_has_prefix (data_ext_x, "BYTERANGE:")) {
|
|
gchar *v = data + 17;
|
|
|
|
if (int64_from_string (v, &v, &size)) {
|
|
if (*v == '@' && !int64_from_string (v + 1, &v, &offset))
|
|
goto next_line;
|
|
} else {
|
|
goto next_line;
|
|
}
|
|
} else {
|
|
GST_LOG ("Ignored line: %s", data);
|
|
}
|
|
} else {
|
|
GST_LOG ("Ignored line: %s", data);
|
|
}
|
|
|
|
next_line:
|
|
if (!end)
|
|
break;
|
|
data = g_utf8_next_char (end); /* skip \n */
|
|
}
|
|
|
|
g_free (current_key);
|
|
current_key = NULL;
|
|
|
|
if (self->files == NULL) {
|
|
GST_ERROR ("Invalid media playlist, it does not contain any media files");
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
self->files = g_list_reverse (self->files);
|
|
|
|
/* calculate the start and end times of this media playlist. */
|
|
{
|
|
GList *walk;
|
|
GstM3U8MediaFile *file;
|
|
GstClockTime duration = 0;
|
|
|
|
for (walk = self->files; walk; walk = walk->next) {
|
|
file = walk->data;
|
|
duration += file->duration;
|
|
if (file->sequence > self->highest_sequence_number) {
|
|
if (self->highest_sequence_number >= 0) {
|
|
/* if an update of the media playlist has been missed, there
|
|
will be a gap between self->highest_sequence_number and the
|
|
first sequence number in this media playlist. In this situation
|
|
assume that the missing fragments had a duration of
|
|
targetduration each */
|
|
self->last_file_end +=
|
|
(file->sequence - self->highest_sequence_number -
|
|
1) * self->targetduration;
|
|
}
|
|
self->last_file_end += file->duration;
|
|
self->highest_sequence_number = file->sequence;
|
|
}
|
|
}
|
|
if (GST_M3U8_IS_LIVE (self)) {
|
|
self->first_file_start = self->last_file_end - duration;
|
|
GST_DEBUG ("Live playlist range %" GST_TIME_FORMAT " -> %"
|
|
GST_TIME_FORMAT, GST_TIME_ARGS (self->first_file_start),
|
|
GST_TIME_ARGS (self->last_file_end));
|
|
}
|
|
self->duration = duration;
|
|
}
|
|
|
|
/* first-time setup */
|
|
if (self->files && self->sequence == -1) {
|
|
GList *file;
|
|
|
|
if (GST_M3U8_IS_LIVE (self)) {
|
|
gint i;
|
|
|
|
file = g_list_last (self->files);
|
|
|
|
/* for live streams, start GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE from
|
|
* the end of the playlist. See section 6.3.3 of HLS draft */
|
|
for (i = 0; i < GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE && file->prev; ++i)
|
|
file = file->prev;
|
|
} else {
|
|
file = g_list_first (self->files);
|
|
}
|
|
self->current_file = file;
|
|
self->sequence = GST_M3U8_MEDIA_FILE (file->data)->sequence;
|
|
self->sequence_position = 0;
|
|
GST_DEBUG ("first sequence: %u", (guint) self->sequence);
|
|
}
|
|
|
|
GST_LOG ("processed media playlist %s, %u fragments", self->name,
|
|
g_list_length (self->files));
|
|
|
|
GST_M3U8_UNLOCK (self);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* call with M3U8_LOCK held */
|
|
static GList *
|
|
m3u8_find_next_fragment (GstM3U8 * m3u8, gboolean forward)
|
|
{
|
|
GstM3U8MediaFile *file;
|
|
GList *l = m3u8->files;
|
|
|
|
if (forward) {
|
|
while (l) {
|
|
file = l->data;
|
|
|
|
if (file->sequence >= m3u8->sequence)
|
|
break;
|
|
|
|
l = l->next;
|
|
}
|
|
} else {
|
|
l = g_list_last (l);
|
|
|
|
while (l) {
|
|
file = l->data;
|
|
|
|
if (file->sequence <= m3u8->sequence)
|
|
break;
|
|
|
|
l = l->prev;
|
|
}
|
|
}
|
|
|
|
return l;
|
|
}
|
|
|
|
GstM3U8MediaFile *
|
|
gst_m3u8_get_next_fragment (GstM3U8 * m3u8, gboolean forward,
|
|
GstClockTime * sequence_position, gboolean * discont)
|
|
{
|
|
GstM3U8MediaFile *file = NULL;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, NULL);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
|
|
GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, m3u8->sequence);
|
|
|
|
if (m3u8->sequence < 0) /* can't happen really */
|
|
goto out;
|
|
|
|
if (m3u8->current_file == NULL)
|
|
m3u8->current_file = m3u8_find_next_fragment (m3u8, forward);
|
|
|
|
if (m3u8->current_file == NULL)
|
|
goto out;
|
|
|
|
file = gst_m3u8_media_file_ref (m3u8->current_file->data);
|
|
|
|
GST_DEBUG ("Got fragment with sequence %u (current sequence %u)",
|
|
(guint) file->sequence, (guint) m3u8->sequence);
|
|
|
|
*sequence_position = m3u8->sequence_position;
|
|
*discont = file->discont || (m3u8->sequence != file->sequence);
|
|
|
|
m3u8->current_file_duration = file->duration;
|
|
m3u8->sequence = file->sequence;
|
|
|
|
out:
|
|
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
|
|
return file;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_has_next_fragment (GstM3U8 * m3u8, gboolean forward)
|
|
{
|
|
gboolean have_next;
|
|
GList *cur;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, FALSE);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
|
|
GST_DEBUG ("Checking next fragment %" G_GINT64_FORMAT,
|
|
m3u8->sequence + (forward ? 1 : -1));
|
|
|
|
if (m3u8->current_file) {
|
|
cur = m3u8->current_file;
|
|
} else {
|
|
cur = m3u8_find_next_fragment (m3u8, forward);
|
|
}
|
|
|
|
have_next = (forward && cur->next) || (!forward && cur->prev);
|
|
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
|
|
return have_next;
|
|
}
|
|
|
|
/* call with M3U8_LOCK held */
|
|
static void
|
|
m3u8_alternate_advance (GstM3U8 * m3u8, gboolean forward)
|
|
{
|
|
gint targetnum = m3u8->sequence;
|
|
GList *tmp;
|
|
GstM3U8MediaFile *mf;
|
|
|
|
/* figure out the target seqnum */
|
|
if (forward)
|
|
targetnum += 1;
|
|
else
|
|
targetnum -= 1;
|
|
|
|
for (tmp = m3u8->files; tmp; tmp = tmp->next) {
|
|
mf = (GstM3U8MediaFile *) tmp->data;
|
|
if (mf->sequence == targetnum)
|
|
break;
|
|
}
|
|
if (tmp == NULL) {
|
|
GST_WARNING ("Can't find next fragment");
|
|
return;
|
|
}
|
|
m3u8->current_file = tmp;
|
|
m3u8->sequence = targetnum;
|
|
m3u8->current_file_duration = GST_M3U8_MEDIA_FILE (tmp->data)->duration;
|
|
}
|
|
|
|
void
|
|
gst_m3u8_advance_fragment (GstM3U8 * m3u8, gboolean forward)
|
|
{
|
|
GstM3U8MediaFile *file;
|
|
|
|
g_return_if_fail (m3u8 != NULL);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
|
|
GST_DEBUG ("Sequence position was %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (m3u8->sequence_position));
|
|
if (GST_CLOCK_TIME_IS_VALID (m3u8->current_file_duration)) {
|
|
/* Advance our position based on the previous fragment we played */
|
|
if (forward)
|
|
m3u8->sequence_position += m3u8->current_file_duration;
|
|
else if (m3u8->current_file_duration < m3u8->sequence_position)
|
|
m3u8->sequence_position -= m3u8->current_file_duration;
|
|
else
|
|
m3u8->sequence_position = 0;
|
|
m3u8->current_file_duration = GST_CLOCK_TIME_NONE;
|
|
GST_DEBUG ("Sequence position now %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (m3u8->sequence_position));
|
|
}
|
|
if (!m3u8->current_file) {
|
|
GList *l;
|
|
|
|
GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, m3u8->sequence);
|
|
for (l = m3u8->files; l != NULL; l = l->next) {
|
|
if (GST_M3U8_MEDIA_FILE (l->data)->sequence == m3u8->sequence) {
|
|
m3u8->current_file = l;
|
|
break;
|
|
}
|
|
}
|
|
if (m3u8->current_file == NULL) {
|
|
GST_DEBUG
|
|
("Could not find current fragment, trying next fragment directly");
|
|
m3u8_alternate_advance (m3u8, forward);
|
|
|
|
/* Resync sequence number if the above has failed for live streams */
|
|
if (m3u8->current_file == NULL && GST_M3U8_IS_LIVE (m3u8)) {
|
|
/* for live streams, start GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE from
|
|
the end of the playlist. See section 6.3.3 of HLS draft */
|
|
gint pos =
|
|
g_list_length (m3u8->files) - GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE;
|
|
m3u8->current_file = g_list_nth (m3u8->files, pos >= 0 ? pos : 0);
|
|
m3u8->current_file_duration =
|
|
GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->duration;
|
|
|
|
GST_WARNING ("Resyncing live playlist");
|
|
}
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
file = GST_M3U8_MEDIA_FILE (m3u8->current_file->data);
|
|
GST_DEBUG ("Advancing from sequence %u", (guint) file->sequence);
|
|
if (forward) {
|
|
m3u8->current_file = m3u8->current_file->next;
|
|
if (m3u8->current_file) {
|
|
m3u8->sequence = GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->sequence;
|
|
} else {
|
|
m3u8->sequence = file->sequence + 1;
|
|
}
|
|
} else {
|
|
m3u8->current_file = m3u8->current_file->prev;
|
|
if (m3u8->current_file) {
|
|
m3u8->sequence = GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->sequence;
|
|
} else {
|
|
m3u8->sequence = file->sequence - 1;
|
|
}
|
|
}
|
|
if (m3u8->current_file) {
|
|
/* Store duration of the fragment we're using to update the position
|
|
* the next time we advance */
|
|
m3u8->current_file_duration =
|
|
GST_M3U8_MEDIA_FILE (m3u8->current_file->data)->duration;
|
|
}
|
|
|
|
out:
|
|
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
}
|
|
|
|
GstClockTime
|
|
gst_m3u8_get_duration (GstM3U8 * m3u8)
|
|
{
|
|
GstClockTime duration = GST_CLOCK_TIME_NONE;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, GST_CLOCK_TIME_NONE);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
|
|
/* We can only get the duration for on-demand streams */
|
|
if (!m3u8->endlist)
|
|
goto out;
|
|
|
|
if (!GST_CLOCK_TIME_IS_VALID (m3u8->duration) && m3u8->files != NULL) {
|
|
GList *f;
|
|
|
|
m3u8->duration = 0;
|
|
for (f = m3u8->files; f != NULL; f = f->next)
|
|
m3u8->duration += GST_M3U8_MEDIA_FILE (f)->duration;
|
|
}
|
|
duration = m3u8->duration;
|
|
|
|
out:
|
|
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
|
|
return duration;
|
|
}
|
|
|
|
GstClockTime
|
|
gst_m3u8_get_target_duration (GstM3U8 * m3u8)
|
|
{
|
|
GstClockTime target_duration;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, GST_CLOCK_TIME_NONE);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
target_duration = m3u8->targetduration;
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
|
|
return target_duration;
|
|
}
|
|
|
|
gchar *
|
|
gst_m3u8_get_uri (GstM3U8 * m3u8)
|
|
{
|
|
gchar *uri;
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
uri = g_strdup (m3u8->uri);
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
|
|
return uri;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_has_variant_playlist (GstM3U8 * m3u8)
|
|
{
|
|
gboolean ret;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, FALSE);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
ret = (m3u8->lists != NULL);
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_is_live (GstM3U8 * m3u8)
|
|
{
|
|
gboolean is_live;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, FALSE);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
is_live = GST_M3U8_IS_LIVE (m3u8);
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
|
|
return is_live;
|
|
}
|
|
|
|
GList *
|
|
gst_m3u8_get_playlist_for_bitrate (GstM3U8 * main, guint bitrate)
|
|
{
|
|
GList *list, *current_variant;
|
|
|
|
GST_M3U8_LOCK (main);
|
|
current_variant = main->current_variant;
|
|
|
|
/* Go to the highest possible bandwidth allowed */
|
|
while (GST_M3U8 (current_variant->data)->bandwidth <= bitrate) {
|
|
list = g_list_next (current_variant);
|
|
if (!list)
|
|
break;
|
|
current_variant = list;
|
|
}
|
|
|
|
while (GST_M3U8 (current_variant->data)->bandwidth > bitrate) {
|
|
list = g_list_previous (current_variant);
|
|
if (!list)
|
|
break;
|
|
current_variant = list;
|
|
}
|
|
GST_M3U8_UNLOCK (main);
|
|
|
|
return current_variant;
|
|
}
|
|
|
|
gchar *
|
|
uri_join (const gchar * uri1, const gchar * uri2)
|
|
{
|
|
gchar *uri_copy, *tmp, *ret = NULL;
|
|
|
|
if (gst_uri_is_valid (uri2))
|
|
return g_strdup (uri2);
|
|
|
|
uri_copy = g_strdup (uri1);
|
|
if (uri2[0] != '/') {
|
|
/* uri2 is a relative uri2 */
|
|
/* look for query params */
|
|
tmp = g_utf8_strchr (uri_copy, -1, '?');
|
|
if (tmp) {
|
|
/* find last / char, ignoring query params */
|
|
tmp = g_utf8_strrchr (uri_copy, tmp - uri_copy, '/');
|
|
} else {
|
|
/* find last / char in URL */
|
|
tmp = g_utf8_strrchr (uri_copy, -1, '/');
|
|
}
|
|
if (!tmp) {
|
|
GST_WARNING ("Can't build a valid uri_copy");
|
|
goto out;
|
|
}
|
|
|
|
*tmp = '\0';
|
|
ret = g_strdup_printf ("%s/%s", uri_copy, uri2);
|
|
} else {
|
|
/* uri2 is an absolute uri2 */
|
|
char *scheme, *hostname;
|
|
|
|
scheme = uri_copy;
|
|
/* find the : in <scheme>:// */
|
|
tmp = g_utf8_strchr (uri_copy, -1, ':');
|
|
if (!tmp) {
|
|
GST_WARNING ("Can't build a valid uri_copy");
|
|
goto out;
|
|
}
|
|
|
|
*tmp = '\0';
|
|
|
|
/* skip :// */
|
|
hostname = tmp + 3;
|
|
|
|
tmp = g_utf8_strchr (hostname, -1, '/');
|
|
if (tmp)
|
|
*tmp = '\0';
|
|
|
|
ret = g_strdup_printf ("%s://%s%s", scheme, hostname, uri2);
|
|
}
|
|
|
|
out:
|
|
g_free (uri_copy);
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_get_seek_range (GstM3U8 * m3u8, gint64 * start, gint64 * stop)
|
|
{
|
|
GstClockTime duration = 0;
|
|
GList *walk;
|
|
GstM3U8MediaFile *file;
|
|
guint count;
|
|
guint min_distance = 0;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, FALSE);
|
|
|
|
GST_M3U8_LOCK (m3u8);
|
|
|
|
if (m3u8->files == NULL)
|
|
goto out;
|
|
|
|
if (GST_M3U8_IS_LIVE (m3u8)) {
|
|
/* min_distance is used to make sure the seek range is never closer than
|
|
GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE fragments from the end of a live
|
|
playlist - see 6.3.3. "Playing the Playlist file" of the HLS draft */
|
|
min_distance = GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE;
|
|
}
|
|
count = g_list_length (m3u8->files);
|
|
|
|
for (walk = m3u8->files; walk && count >= min_distance; walk = walk->next) {
|
|
file = walk->data;
|
|
--count;
|
|
duration += file->duration;
|
|
}
|
|
|
|
if (duration <= 0)
|
|
goto out;
|
|
|
|
*start = m3u8->first_file_start;
|
|
*stop = *start + duration;
|
|
|
|
out:
|
|
|
|
GST_M3U8_UNLOCK (m3u8);
|
|
return (duration > 0);
|
|
}
|
|
|
|
static gboolean
|
|
gst_m3u8_update_master_playlist (GstM3U8 * self, gchar * data)
|
|
{
|
|
GstM3U8 *list;
|
|
gchar *end;
|
|
gint val;
|
|
|
|
g_return_val_if_fail (self != NULL, FALSE);
|
|
g_return_val_if_fail (data != NULL, FALSE);
|
|
|
|
GST_M3U8_LOCK (self);
|
|
|
|
/* 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");
|
|
g_free (data);
|
|
GST_M3U8_UNLOCK (self);
|
|
return TRUE;
|
|
}
|
|
|
|
if (!g_str_has_prefix (data, "#EXTM3U")) {
|
|
GST_WARNING ("Data doesn't start with #EXTM3U");
|
|
g_free (data);
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
if (strstr (data, "\n#EXTINF:") != NULL) {
|
|
GST_WARNING ("This is a media playlist, not a master playlist!");
|
|
g_free (data);
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
GST_TRACE ("data:\n%s", data);
|
|
|
|
g_free (self->last_data);
|
|
self->last_data = data;
|
|
|
|
self->duration = GST_CLOCK_TIME_NONE;
|
|
|
|
list = NULL;
|
|
data += 7;
|
|
while (TRUE) {
|
|
gchar *r;
|
|
|
|
end = g_utf8_strchr (data, -1, '\n');
|
|
if (end)
|
|
*end = '\0';
|
|
|
|
r = g_utf8_strchr (data, -1, '\r');
|
|
if (r)
|
|
*r = '\0';
|
|
|
|
if (data[0] != '#' && data[0] != '\0') {
|
|
gchar *name = data;
|
|
|
|
if (list == NULL) {
|
|
GST_LOG ("%s: got line without EXT-STREAM-INF, dropping", data);
|
|
goto next_line;
|
|
}
|
|
|
|
data = uri_join (self->base_uri ? self->base_uri : self->uri, data);
|
|
if (data == NULL)
|
|
goto next_line;
|
|
|
|
if (g_list_find_custom (self->lists, data,
|
|
(GCompareFunc) _m3u8_compare_uri)) {
|
|
GST_DEBUG ("Already have a list with this URI");
|
|
gst_m3u8_unref (list);
|
|
g_free (data);
|
|
} else {
|
|
gst_m3u8_take_uri (list, data, NULL, g_strdup (name));
|
|
self->lists = g_list_append (self->lists, list);
|
|
}
|
|
list = NULL;
|
|
} 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:") ||
|
|
g_str_has_prefix (data, "#EXT-X-I-FRAME-STREAM-INF:")) {
|
|
gboolean iframe = g_str_has_prefix (data + 7, "I-FRAME");
|
|
GstM3U8 *new_list;
|
|
gchar *v, *a;
|
|
|
|
new_list = gst_m3u8_new ();
|
|
new_list->iframe = iframe;
|
|
data = data + (iframe ? 26 : 18);
|
|
while (data && parse_attributes (&data, &a, &v)) {
|
|
if (g_str_equal (a, "BANDWIDTH")) {
|
|
if (!int_from_string (v, NULL, &new_list->bandwidth))
|
|
GST_WARNING ("Error while reading BANDWIDTH");
|
|
} else if (g_str_equal (a, "PROGRAM-ID")) {
|
|
if (!int_from_string (v, NULL, &new_list->program_id))
|
|
GST_WARNING ("Error while reading PROGRAM-ID");
|
|
} else if (g_str_equal (a, "CODECS")) {
|
|
g_free (new_list->codecs);
|
|
new_list->codecs = g_strdup (v);
|
|
} else if (g_str_equal (a, "RESOLUTION")) {
|
|
if (!int_from_string (v, &v, &new_list->width))
|
|
GST_WARNING ("Error while reading RESOLUTION width");
|
|
if (!v || *v != 'x') {
|
|
GST_WARNING ("Missing height");
|
|
} else {
|
|
v = g_utf8_next_char (v);
|
|
if (!int_from_string (v, NULL, &new_list->height))
|
|
GST_WARNING ("Error while reading RESOLUTION height");
|
|
}
|
|
} else if (iframe && g_str_equal (a, "URI")) {
|
|
gchar *name;
|
|
gchar *uri = g_strdup (v);
|
|
gchar *urip = uri;
|
|
|
|
uri = unquote_string (uri);
|
|
if (uri) {
|
|
uri = uri_join (self->base_uri ? self->base_uri : self->uri, uri);
|
|
if (uri == NULL) {
|
|
g_free (urip);
|
|
continue;
|
|
}
|
|
name = g_strdup (uri);
|
|
|
|
gst_m3u8_take_uri (new_list, uri, NULL, name);
|
|
} else {
|
|
GST_WARNING
|
|
("Cannot remove quotation marks from i-frame-stream URI");
|
|
}
|
|
g_free (urip);
|
|
}
|
|
}
|
|
|
|
if (iframe) {
|
|
if (g_list_find_custom (self->iframe_lists, new_list->uri,
|
|
(GCompareFunc) _m3u8_compare_uri)) {
|
|
GST_DEBUG ("Already have a list with this URI");
|
|
gst_m3u8_unref (new_list);
|
|
} else {
|
|
self->iframe_lists = g_list_append (self->iframe_lists, new_list);
|
|
}
|
|
} else {
|
|
if (list != NULL) {
|
|
GST_WARNING ("Found a list without a uri..., dropping");
|
|
gst_m3u8_unref (list);
|
|
}
|
|
list = new_list;
|
|
}
|
|
} else if (*data != '\0') {
|
|
GST_LOG ("Ignored line: %s", data);
|
|
}
|
|
|
|
next_line:
|
|
if (!end)
|
|
break;
|
|
data = g_utf8_next_char (end); /* skip \n */
|
|
}
|
|
|
|
/* reorder playlists by bitrate */
|
|
if (self->lists) {
|
|
gchar *top_variant_uri = NULL;
|
|
gboolean iframe = FALSE;
|
|
|
|
if (!self->current_variant) {
|
|
top_variant_uri = GST_M3U8 (self->lists->data)->uri;
|
|
} else {
|
|
top_variant_uri = GST_M3U8 (self->current_variant->data)->uri;
|
|
iframe = GST_M3U8 (self->current_variant->data)->iframe;
|
|
}
|
|
|
|
self->lists =
|
|
g_list_sort (self->lists,
|
|
(GCompareFunc) gst_m3u8_compare_playlist_by_bitrate);
|
|
|
|
self->iframe_lists =
|
|
g_list_sort (self->iframe_lists,
|
|
(GCompareFunc) gst_m3u8_compare_playlist_by_bitrate);
|
|
|
|
if (iframe)
|
|
self->current_variant =
|
|
g_list_find_custom (self->iframe_lists, top_variant_uri,
|
|
(GCompareFunc) _m3u8_compare_uri);
|
|
else
|
|
self->current_variant = g_list_find_custom (self->lists, top_variant_uri,
|
|
(GCompareFunc) _m3u8_compare_uri);
|
|
}
|
|
|
|
if (self->lists == NULL) {
|
|
GST_ERROR ("Invalid master playlist, it does not contain any streams");
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
GST_DEBUG ("parsed master playlist with %d streams and %d I-frame streams",
|
|
g_list_length (self->lists), g_list_length (self->iframe_lists));
|
|
|
|
GST_M3U8_UNLOCK (self);
|
|
|
|
return TRUE;
|
|
}
|