mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-27 02:30:35 +00:00
e0b4290998
If a (master) playlist contains a variant list entry without a URI then during parsing of the next variant list entry we are a) leaking the entry we're currently parsing (new_list), and b) free'ing the pointer to the previous list entry (list) without updating the pointer. Hence when then adding the URI for the latest parsed entry, incorrect information is stored, as the information is used from 'list' which is not valid memory anymore, also leading to crashes. Fix this by correctly storing the new variant list entry pointer as needed. https://bugzilla.gnome.org/show_bug.cgi?id=756861
1423 lines
37 KiB
C
1423 lines
37 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 "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 (GstM3U8Client * client, GstM3U8 * m3u8,
|
|
gchar * data, gboolean * updated);
|
|
static GstM3U8MediaFile *gst_m3u8_media_file_new (gchar * uri,
|
|
gchar * title, GstClockTime duration, guint sequence);
|
|
static void gst_m3u8_media_file_free (GstM3U8MediaFile * self);
|
|
gchar *uri_join (const gchar * uri, const gchar * path);
|
|
|
|
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, gchar * base_uri, gchar * name)
|
|
{
|
|
g_return_if_fail (self != NULL);
|
|
|
|
g_free (self->uri);
|
|
self->uri = uri;
|
|
|
|
g_free (self->base_uri);
|
|
self->base_uri = base_uri;
|
|
|
|
g_free (self->name);
|
|
self->name = name;
|
|
}
|
|
|
|
static void
|
|
gst_m3u8_free (GstM3U8 * self)
|
|
{
|
|
g_return_if_fail (self != NULL);
|
|
|
|
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_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_list_foreach (self->iframe_lists, (GFunc) gst_m3u8_free, 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;
|
|
|
|
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->key);
|
|
g_free (self);
|
|
}
|
|
|
|
static GstM3U8MediaFile *
|
|
gst_m3u8_media_file_copy (const GstM3U8MediaFile * self, gpointer user_data)
|
|
{
|
|
g_return_val_if_fail (self != NULL, NULL);
|
|
|
|
return gst_m3u8_media_file_new (g_strdup (self->uri), g_strdup (self->title),
|
|
self->duration, self->sequence);
|
|
}
|
|
|
|
static GstM3U8 *
|
|
_m3u8_copy (const GstM3U8 * self)
|
|
{
|
|
GstM3U8 *dup;
|
|
|
|
g_return_val_if_fail (self != NULL, NULL);
|
|
|
|
dup = gst_m3u8_new ();
|
|
dup->uri = g_strdup (self->uri);
|
|
dup->base_uri = g_strdup (self->base_uri);
|
|
dup->name = g_strdup (self->name);
|
|
dup->endlist = self->endlist;
|
|
dup->version = self->version;
|
|
dup->targetduration = self->targetduration;
|
|
dup->allowcache = self->allowcache;
|
|
dup->bandwidth = self->bandwidth;
|
|
dup->program_id = self->program_id;
|
|
dup->codecs = g_strdup (self->codecs);
|
|
dup->width = self->width;
|
|
dup->height = self->height;
|
|
dup->iframe = self->iframe;
|
|
dup->files =
|
|
g_list_copy_deep (self->files, (GCopyFunc) gst_m3u8_media_file_copy,
|
|
NULL);
|
|
|
|
/* private */
|
|
dup->last_data = g_strdup (self->last_data);
|
|
dup->lists = g_list_copy_deep (self->lists, (GCopyFunc) _m3u8_copy, NULL);
|
|
dup->iframe_lists =
|
|
g_list_copy_deep (self->iframe_lists, (GCopyFunc) _m3u8_copy, NULL);
|
|
/* NOTE: current_variant will get set in gst_m3u8_copy () */
|
|
dup->mediasequence = self->mediasequence;
|
|
return dup;
|
|
}
|
|
|
|
static GstM3U8 *
|
|
gst_m3u8_copy (const GstM3U8 * self)
|
|
{
|
|
GList *entry;
|
|
guint n;
|
|
|
|
GstM3U8 *dup = _m3u8_copy (self);
|
|
|
|
if (self->current_variant != NULL) {
|
|
for (n = 0, entry = self->lists; entry; entry = entry->next, n++) {
|
|
if (entry == self->current_variant) {
|
|
dup->current_variant = g_list_nth (dup->lists, n);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!dup->current_variant) {
|
|
for (n = 0, entry = self->iframe_lists; entry; entry = entry->next, n++) {
|
|
if (entry == self->current_variant) {
|
|
dup->current_variant = g_list_nth (dup->iframe_lists, n);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!dup->current_variant) {
|
|
GST_ERROR ("Failed to determine current playlist");
|
|
}
|
|
}
|
|
}
|
|
|
|
return dup;
|
|
}
|
|
|
|
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;
|
|
|
|
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) {
|
|
*v = g_utf8_next_char (*v);
|
|
*p = '\0';
|
|
} else {
|
|
GST_WARNING ("missing = after attribute");
|
|
return FALSE;
|
|
}
|
|
|
|
*ptr = end;
|
|
return TRUE;
|
|
}
|
|
|
|
static gchar *
|
|
unquote_string (gchar * string)
|
|
{
|
|
gchar *string_ret;
|
|
|
|
string_ret = strchr (string, '"');
|
|
if (string_ret != NULL) {
|
|
/* found initialization quotation mark of string */
|
|
string = string_ret + 1;
|
|
string_ret = strchr (string, '"');
|
|
if (string_ret != NULL) {
|
|
/* found finalizing quotation mark of string */
|
|
string_ret[0] = '\0';
|
|
} else {
|
|
GST_WARNING
|
|
("wrong string unqouting - cannot find finalizing quotation mark");
|
|
return NULL;
|
|
}
|
|
}
|
|
return string;
|
|
}
|
|
|
|
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 (GstM3U8Client * client, GstM3U8 * self, gchar * data,
|
|
gboolean * updated)
|
|
{
|
|
gint val;
|
|
GstClockTime duration;
|
|
gchar *title, *end;
|
|
gboolean discontinuity = FALSE;
|
|
GstM3U8 *list;
|
|
gchar *current_key = NULL;
|
|
gboolean have_iv = FALSE;
|
|
guint8 iv[16] = { 0, };
|
|
gint64 size = -1, offset = -1;
|
|
|
|
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;
|
|
}
|
|
|
|
GST_TRACE ("data:\n%s", data);
|
|
|
|
g_free (self->last_data);
|
|
self->last_data = data;
|
|
|
|
client->current_file = NULL;
|
|
if (self->files) {
|
|
g_list_foreach (self->files, (GFunc) gst_m3u8_media_file_free, NULL);
|
|
g_list_free (self->files);
|
|
self->files = NULL;
|
|
}
|
|
client->duration = GST_CLOCK_TIME_NONE;
|
|
self->mediasequence = 0;
|
|
|
|
/* By default, allow caching */
|
|
self->allowcache = TRUE;
|
|
|
|
list = NULL;
|
|
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') {
|
|
gchar *name = data;
|
|
if (duration <= 0 && list == NULL) {
|
|
GST_LOG ("%s: got line without EXTINF or EXTSTREAMINF, dropping", data);
|
|
goto next_line;
|
|
}
|
|
|
|
data = uri_join (self->base_uri ? self->base_uri : self->uri, data);
|
|
if (data == NULL)
|
|
goto next_line;
|
|
|
|
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, NULL, g_strdup (name));
|
|
self->lists = g_list_append (self->lists, list);
|
|
}
|
|
list = NULL;
|
|
} else {
|
|
GstM3U8MediaFile *file;
|
|
file =
|
|
gst_m3u8_media_file_new (data, title, duration,
|
|
self->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, "STREAM-INF:") ||
|
|
g_str_has_prefix (data_ext_x, "I-FRAME-STREAM-INF:")) {
|
|
gchar *v, *a;
|
|
gboolean iframe = g_str_has_prefix (data_ext_x, "I-FRAME-STREAM-INF:");
|
|
GstM3U8 *new_list;
|
|
|
|
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_set_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_free (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_free (list);
|
|
}
|
|
list = new_list;
|
|
}
|
|
} 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))
|
|
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:")) {
|
|
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")) {
|
|
gchar *key = g_strdup (v);
|
|
gchar *keyp = key;
|
|
|
|
key = unquote_string (key);
|
|
if (key) {
|
|
current_key =
|
|
uri_join (self->base_uri ? self->base_uri : self->uri, key);
|
|
} else {
|
|
GST_WARNING
|
|
("Cannot remove quotation marks from decryption key URI");
|
|
}
|
|
g_free (keyp);
|
|
} 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;
|
|
|
|
self->files = g_list_reverse (self->files);
|
|
|
|
/* 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);
|
|
}
|
|
/* calculate the start and end times of this media playlist. */
|
|
if (self->files) {
|
|
GList *walk;
|
|
GstM3U8MediaFile *file;
|
|
GstClockTime duration = 0;
|
|
|
|
for (walk = self->files; walk; walk = walk->next) {
|
|
file = walk->data;
|
|
duration += file->duration;
|
|
if (file->sequence > client->highest_sequence_number) {
|
|
if (client->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 */
|
|
client->last_file_end +=
|
|
(file->sequence - client->highest_sequence_number -
|
|
1) * self->targetduration;
|
|
}
|
|
client->last_file_end += file->duration;
|
|
client->highest_sequence_number = file->sequence;
|
|
}
|
|
}
|
|
if (GST_M3U8_CLIENT_IS_LIVE (client)) {
|
|
client->first_file_start = client->last_file_end - duration;
|
|
GST_DEBUG ("Live playlist range %" GST_TIME_FORMAT " -> %"
|
|
GST_TIME_FORMAT, GST_TIME_ARGS (client->first_file_start),
|
|
GST_TIME_ARGS (client->last_file_end));
|
|
}
|
|
client->duration = duration;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
GstM3U8Client *
|
|
gst_m3u8_client_new (const gchar * uri, const gchar * base_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->current_file = NULL;
|
|
client->current_file_duration = GST_CLOCK_TIME_NONE;
|
|
client->sequence = -1;
|
|
client->sequence_position = 0;
|
|
client->update_failed_count = 0;
|
|
client->highest_sequence_number = -1;
|
|
client->duration = GST_CLOCK_TIME_NONE;
|
|
g_mutex_init (&client->lock);
|
|
gst_m3u8_set_uri (client->main, g_strdup (uri), g_strdup (base_uri), NULL);
|
|
|
|
return client;
|
|
}
|
|
|
|
void
|
|
gst_m3u8_client_free (GstM3U8Client * self)
|
|
{
|
|
g_return_if_fail (self != NULL);
|
|
|
|
gst_m3u8_free (self->main);
|
|
g_mutex_clear (&self->lock);
|
|
g_free (self);
|
|
}
|
|
|
|
void
|
|
gst_m3u8_client_set_current (GstM3U8Client * self, GstM3U8 * m3u8)
|
|
{
|
|
g_return_if_fail (self != NULL);
|
|
|
|
GST_M3U8_CLIENT_LOCK (self);
|
|
if (m3u8 != self->current) {
|
|
self->current = m3u8;
|
|
self->update_failed_count = 0;
|
|
self->duration = GST_CLOCK_TIME_NONE;
|
|
self->current_file = NULL;
|
|
}
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_update (GstM3U8Client * self, gchar * data)
|
|
{
|
|
GstM3U8 *m3u8;
|
|
gboolean updated = FALSE;
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (self != NULL, FALSE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (self);
|
|
m3u8 = self->current ? self->current : self->main;
|
|
|
|
if (!gst_m3u8_update (self, m3u8, data, &updated))
|
|
goto out;
|
|
|
|
if (!updated) {
|
|
self->update_failed_count++;
|
|
goto out;
|
|
}
|
|
|
|
if (self->current && !self->current->files) {
|
|
GST_ERROR ("Invalid media playlist, it does not contain any media files");
|
|
goto out;
|
|
}
|
|
|
|
/* 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) {
|
|
if (GST_M3U8_CLIENT_IS_LIVE (self)) {
|
|
/* 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;
|
|
self->current_file = g_list_nth (m3u8->files, pos >= 0 ? pos : 0);
|
|
} else {
|
|
self->current_file = g_list_first (m3u8->files);
|
|
}
|
|
self->sequence = GST_M3U8_MEDIA_FILE (self->current_file->data)->sequence;
|
|
self->sequence_position = 0;
|
|
GST_DEBUG ("Setting first sequence at %u", (guint) self->sequence);
|
|
}
|
|
|
|
ret = TRUE;
|
|
out:
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
return ret;
|
|
}
|
|
|
|
static gint
|
|
_find_m3u8_list_match (const GstM3U8 * a, const GstM3U8 * b)
|
|
{
|
|
if (g_strcmp0 (a->name, b->name) == 0 &&
|
|
a->bandwidth == b->bandwidth &&
|
|
a->program_id == b->program_id &&
|
|
g_strcmp0 (a->codecs, b->codecs) == 0 &&
|
|
a->width == b->width &&
|
|
a->height == b->height && a->iframe == b->iframe) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_update_variant_playlist (GstM3U8Client * self, gchar * data,
|
|
const gchar * uri, const gchar * base_uri)
|
|
{
|
|
gboolean ret = FALSE;
|
|
GList *list_entry, *unmatched_lists;
|
|
GstM3U8Client *new_client;
|
|
GstM3U8 *old;
|
|
|
|
g_return_val_if_fail (self != NULL, FALSE);
|
|
|
|
new_client = gst_m3u8_client_new (uri, base_uri);
|
|
if (gst_m3u8_client_update (new_client, data)) {
|
|
if (!new_client->main->lists) {
|
|
GST_ERROR
|
|
("Cannot update variant playlist: New playlist is not a variant playlist");
|
|
gst_m3u8_client_free (new_client);
|
|
return FALSE;
|
|
}
|
|
|
|
GST_M3U8_CLIENT_LOCK (self);
|
|
|
|
if (!self->main->lists) {
|
|
GST_ERROR
|
|
("Cannot update variant playlist: Current playlist is not a variant playlist");
|
|
goto out;
|
|
}
|
|
|
|
/* Now see if the variant playlist still has the same lists */
|
|
unmatched_lists = g_list_copy (self->main->lists);
|
|
for (list_entry = new_client->main->lists; list_entry;
|
|
list_entry = list_entry->next) {
|
|
GList *match = g_list_find_custom (unmatched_lists, list_entry->data,
|
|
(GCompareFunc) _find_m3u8_list_match);
|
|
if (match)
|
|
unmatched_lists = g_list_remove_link (unmatched_lists, match);
|
|
}
|
|
|
|
if (unmatched_lists != NULL) {
|
|
GST_WARNING ("Unable to match all playlists");
|
|
|
|
for (list_entry = unmatched_lists; list_entry;
|
|
list_entry = list_entry->next) {
|
|
if (list_entry->data == self->current) {
|
|
GST_WARNING ("Unable to match current playlist");
|
|
}
|
|
}
|
|
|
|
g_list_free (unmatched_lists);
|
|
}
|
|
|
|
/* Switch out the variant playlist */
|
|
old = self->main;
|
|
|
|
self->main = gst_m3u8_copy (new_client->main);
|
|
if (self->main->lists)
|
|
self->current = self->main->current_variant->data;
|
|
else
|
|
self->current = self->main;
|
|
|
|
gst_m3u8_free (old);
|
|
|
|
ret = TRUE;
|
|
|
|
out:
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
}
|
|
|
|
gst_m3u8_client_free (new_client);
|
|
return ret;
|
|
}
|
|
|
|
static GList *
|
|
find_next_fragment (GstM3U8Client * client, GList * l, gboolean forward)
|
|
{
|
|
GstM3U8MediaFile *file;
|
|
|
|
if (forward) {
|
|
while (l) {
|
|
file = l->data;
|
|
|
|
if (file->sequence >= client->sequence)
|
|
break;
|
|
|
|
l = l->next;
|
|
}
|
|
} else {
|
|
l = g_list_last (l);
|
|
|
|
while (l) {
|
|
file = l->data;
|
|
|
|
if (file->sequence <= client->sequence)
|
|
break;
|
|
|
|
l = l->prev;
|
|
}
|
|
}
|
|
|
|
return l;
|
|
}
|
|
|
|
static gboolean
|
|
has_next_fragment (GstM3U8Client * client, GList * l, gboolean forward)
|
|
{
|
|
l = find_next_fragment (client, l, forward);
|
|
|
|
if (l) {
|
|
return (forward && l->next) || (!forward && l->prev);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_get_next_fragment (GstM3U8Client * client,
|
|
gboolean * discontinuity, gchar ** uri, GstClockTime * duration,
|
|
GstClockTime * timestamp, gint64 * range_start, gint64 * range_end,
|
|
gchar ** key, guint8 ** iv, gboolean forward)
|
|
{
|
|
GstM3U8MediaFile *file;
|
|
|
|
g_return_val_if_fail (client != NULL, FALSE);
|
|
g_return_val_if_fail (client->current != NULL, FALSE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, client->sequence);
|
|
if (client->sequence < 0) {
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return FALSE;
|
|
}
|
|
if (!client->current_file) {
|
|
client->current_file =
|
|
find_next_fragment (client, client->current->files, forward);
|
|
}
|
|
|
|
if (!client->current_file) {
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return FALSE;
|
|
}
|
|
|
|
file = GST_M3U8_MEDIA_FILE (client->current_file->data);
|
|
GST_DEBUG ("Got fragment with sequence %u (client sequence %u)",
|
|
(guint) file->sequence, (guint) client->sequence);
|
|
|
|
client->current_file_duration = file->duration;
|
|
if (timestamp)
|
|
*timestamp = client->sequence_position;
|
|
|
|
if (discontinuity)
|
|
*discontinuity = client->sequence != file->sequence || file->discont;
|
|
if (uri)
|
|
*uri = g_strdup (file->uri);
|
|
if (duration)
|
|
*duration = file->duration;
|
|
if (range_start)
|
|
*range_start = file->offset;
|
|
if (range_end)
|
|
*range_end = file->size != -1 ? file->offset + file->size - 1 : -1;
|
|
if (key)
|
|
*key = g_strdup (file->key);
|
|
if (iv) {
|
|
*iv = g_new (guint8, sizeof (file->iv));
|
|
memcpy (*iv, file->iv, sizeof (file->iv));
|
|
}
|
|
|
|
client->sequence = file->sequence;
|
|
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_has_next_fragment (GstM3U8Client * client, gboolean forward)
|
|
{
|
|
gboolean ret;
|
|
|
|
g_return_val_if_fail (client != NULL, FALSE);
|
|
g_return_val_if_fail (client->current != NULL, FALSE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
GST_DEBUG ("Checking if has next fragment %" G_GINT64_FORMAT,
|
|
client->sequence + (forward ? 1 : -1));
|
|
if (client->current_file) {
|
|
ret =
|
|
(forward ? client->current_file->next : client->current_file->prev) !=
|
|
NULL;
|
|
} else {
|
|
ret = has_next_fragment (client, client->current->files, forward);
|
|
}
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
alternate_advance (GstM3U8Client * client, gboolean forward)
|
|
{
|
|
gint targetnum = client->sequence;
|
|
GList *tmp;
|
|
GstM3U8MediaFile *mf;
|
|
|
|
/* figure out the target seqnum */
|
|
if (forward)
|
|
targetnum += 1;
|
|
else
|
|
targetnum -= 1;
|
|
|
|
for (tmp = client->current->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;
|
|
}
|
|
client->current_file = tmp;
|
|
client->sequence = targetnum;
|
|
client->current_file_duration =
|
|
GST_M3U8_MEDIA_FILE (client->current_file->data)->duration;
|
|
}
|
|
|
|
void
|
|
gst_m3u8_client_advance_fragment (GstM3U8Client * client, gboolean forward)
|
|
{
|
|
GstM3U8MediaFile *file;
|
|
|
|
g_return_if_fail (client != NULL);
|
|
g_return_if_fail (client->current != NULL);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
GST_DEBUG ("Sequence position was %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (client->sequence_position));
|
|
if (GST_CLOCK_TIME_IS_VALID (client->current_file_duration)) {
|
|
/* Advance our position based on the previous fragment we played */
|
|
if (forward)
|
|
client->sequence_position += client->current_file_duration;
|
|
else if (client->current_file_duration < client->sequence_position)
|
|
client->sequence_position -= client->current_file_duration;
|
|
else
|
|
client->sequence_position = 0;
|
|
client->current_file_duration = GST_CLOCK_TIME_NONE;
|
|
GST_DEBUG ("Sequence position now %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (client->sequence_position));
|
|
}
|
|
if (!client->current_file) {
|
|
GList *l;
|
|
|
|
GST_DEBUG ("Looking for fragment %" G_GINT64_FORMAT, client->sequence);
|
|
for (l = client->current->files; l != NULL; l = l->next) {
|
|
if (GST_M3U8_MEDIA_FILE (l->data)->sequence == client->sequence) {
|
|
client->current_file = l;
|
|
break;
|
|
}
|
|
}
|
|
if (client->current_file == NULL) {
|
|
GST_DEBUG
|
|
("Could not find current fragment, trying next fragment directly");
|
|
alternate_advance (client, forward);
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return;
|
|
}
|
|
}
|
|
|
|
file = GST_M3U8_MEDIA_FILE (client->current_file->data);
|
|
GST_DEBUG ("Advancing from sequence %u", (guint) file->sequence);
|
|
if (forward) {
|
|
client->current_file = client->current_file->next;
|
|
if (client->current_file) {
|
|
client->sequence =
|
|
GST_M3U8_MEDIA_FILE (client->current_file->data)->sequence;
|
|
} else {
|
|
client->sequence = file->sequence + 1;
|
|
}
|
|
} else {
|
|
client->current_file = client->current_file->prev;
|
|
if (client->current_file) {
|
|
client->sequence =
|
|
GST_M3U8_MEDIA_FILE (client->current_file->data)->sequence;
|
|
} else {
|
|
client->sequence = file->sequence - 1;
|
|
}
|
|
}
|
|
if (client->current_file) {
|
|
/* Store duration of the fragment we're using to update the position
|
|
* the next time we advance */
|
|
client->current_file_duration =
|
|
GST_M3U8_MEDIA_FILE (client->current_file->data)->duration;
|
|
}
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
}
|
|
|
|
static void
|
|
_sum_duration (GstM3U8MediaFile * self, GstClockTime * duration)
|
|
{
|
|
*duration += self->duration;
|
|
}
|
|
|
|
GstClockTime
|
|
gst_m3u8_client_get_duration (GstM3U8Client * client)
|
|
{
|
|
GstClockTime duration = GST_CLOCK_TIME_NONE;
|
|
|
|
g_return_val_if_fail (client != NULL, GST_CLOCK_TIME_NONE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
/* We can only get the duration for on-demand streams */
|
|
if (!client->current || !client->current->endlist) {
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return GST_CLOCK_TIME_NONE;
|
|
}
|
|
|
|
if (!GST_CLOCK_TIME_IS_VALID (client->duration) && client->current->files) {
|
|
client->duration = 0;
|
|
g_list_foreach (client->current->files, (GFunc) _sum_duration,
|
|
&client->duration);
|
|
}
|
|
duration = client->duration;
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
|
|
return duration;
|
|
}
|
|
|
|
GstClockTime
|
|
gst_m3u8_client_get_target_duration (GstM3U8Client * client)
|
|
{
|
|
GstClockTime duration = 0;
|
|
|
|
g_return_val_if_fail (client != NULL, GST_CLOCK_TIME_NONE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
duration = client->current->targetduration;
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return duration;
|
|
}
|
|
|
|
gchar *
|
|
gst_m3u8_client_get_uri (GstM3U8Client * client)
|
|
{
|
|
gchar *uri;
|
|
|
|
g_return_val_if_fail (client != NULL, NULL);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
uri = client->main ? g_strdup (client->main->uri) : NULL;
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return uri;
|
|
}
|
|
|
|
gchar *
|
|
gst_m3u8_client_get_current_uri (GstM3U8Client * client)
|
|
{
|
|
gchar *uri;
|
|
|
|
g_return_val_if_fail (client != NULL, NULL);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
uri = g_strdup (client->current->uri);
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return uri;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_has_main (GstM3U8Client * client)
|
|
{
|
|
gboolean ret;
|
|
|
|
g_return_val_if_fail (client != NULL, FALSE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
if (client->main)
|
|
ret = TRUE;
|
|
else
|
|
ret = FALSE;
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_has_variant_playlist (GstM3U8Client * client)
|
|
{
|
|
gboolean ret;
|
|
|
|
g_return_val_if_fail (client != NULL, FALSE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
ret = (client->main->lists != NULL);
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_is_live (GstM3U8Client * client)
|
|
{
|
|
gboolean ret;
|
|
|
|
g_return_val_if_fail (client != NULL, FALSE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
ret = GST_M3U8_CLIENT_IS_LIVE (client);
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return ret;
|
|
}
|
|
|
|
GList *
|
|
gst_m3u8_client_get_playlist_for_bitrate (GstM3U8Client * client, guint bitrate)
|
|
{
|
|
GList *list, *current_variant;
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
current_variant = client->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_CLIENT_UNLOCK (client);
|
|
|
|
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;
|
|
}
|
|
|
|
guint64
|
|
gst_m3u8_client_get_current_fragment_duration (GstM3U8Client * client)
|
|
{
|
|
guint64 dur = GST_CLOCK_TIME_NONE;
|
|
GList *l;
|
|
|
|
g_return_val_if_fail (client != NULL, 0);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
|
|
for (l = client->current->files; l != NULL; l = l->next) {
|
|
GstM3U8MediaFile *file = l->data;
|
|
|
|
if (file->sequence == client->sequence) {
|
|
dur = file->duration;
|
|
break;
|
|
}
|
|
}
|
|
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return dur;
|
|
}
|
|
|
|
gboolean
|
|
gst_m3u8_client_get_seek_range (GstM3U8Client * client, gint64 * start,
|
|
gint64 * stop)
|
|
{
|
|
GstClockTime duration = 0;
|
|
GList *walk;
|
|
GstM3U8MediaFile *file;
|
|
guint count;
|
|
|
|
g_return_val_if_fail (client != NULL, FALSE);
|
|
|
|
GST_M3U8_CLIENT_LOCK (client);
|
|
|
|
if (client->current == NULL || client->current->files == NULL) {
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return FALSE;
|
|
}
|
|
|
|
count = g_list_length (client->current->files);
|
|
|
|
/* count is used to make sure the seek range is never closer than
|
|
GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE fragments from the end of the
|
|
playlist - see 6.3.3. "Playing the Playlist file" of the HLS draft */
|
|
for (walk = client->current->files;
|
|
walk && count >= GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE; walk = walk->next) {
|
|
file = walk->data;
|
|
--count;
|
|
duration += file->duration;
|
|
}
|
|
|
|
if (duration <= 0) {
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return FALSE;
|
|
}
|
|
*start = client->first_file_start;
|
|
*stop = *start + duration;
|
|
GST_M3U8_CLIENT_UNLOCK (client);
|
|
return TRUE;
|
|
}
|