mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-25 09:40:37 +00:00
075ceffd9b
Without failing, we would play back random parts of the stream which is arguably a worse user experience, and failing is also recommended by the spec here. And also handle live streams without any media sequence numbers at all properly, that is, make sure the sequence numbers are increasing instead of starting again at 0 every time. https://bugzilla.gnome.org/show_bug.cgi?id=775665
1765 lines
47 KiB
C
1765 lines
47 KiB
C
/* GStreamer
|
|
* Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
|
|
* Copyright (C) 2015 Tim-Philipp Müller <tim@centricular.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);
|
|
static gchar *uri_join (const gchar * uri, const gchar * path);
|
|
|
|
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_list_foreach (self->files, (GFunc) gst_m3u8_media_file_unref, NULL);
|
|
g_list_free (self->files);
|
|
|
|
g_free (self->last_data);
|
|
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
|
|
gst_hls_variant_stream_compare_by_bitrate (gconstpointer a, gconstpointer b)
|
|
{
|
|
const GstHLSVariantStream *vs_a = (const GstHLSVariantStream *) a;
|
|
const GstHLSVariantStream *vs_b = (const GstHLSVariantStream *) b;
|
|
|
|
if (vs_a->bandwidth == vs_b->bandwidth)
|
|
return g_strcmp0 (vs_a->name, vs_b->name);
|
|
|
|
return vs_a->bandwidth - vs_b->bandwidth;
|
|
}
|
|
|
|
static gboolean
|
|
gst_m3u8_update_check_consistent_media_seqnums (GstM3U8 * self,
|
|
gboolean have_mediasequence, GList * previous_files)
|
|
{
|
|
if (!previous_files)
|
|
return TRUE;
|
|
|
|
/* If we have MEDIA-SEQUENCE, ensure that it's consistent. If it is not,
|
|
* the client SHOULD halt playback (6.3.4), which is what we do then.
|
|
*
|
|
* If we don't have MEDIA-SEQUENCE, we check URIs in the previous and
|
|
* current playlist to calculate the/a correct MEDIA-SEQUENCE for the new
|
|
* playlist in relation to the old. That is, same URIs get the same number
|
|
* and later URIs get higher numbers */
|
|
if (have_mediasequence) {
|
|
GList *l, *m;
|
|
GstM3U8MediaFile *f1 = NULL, *f2 = NULL;
|
|
|
|
/* Find first case of higher/equal sequence number in new playlist or
|
|
* same URI. From there on we can linearly step ahead */
|
|
for (l = self->files; l; l = l->next) {
|
|
gboolean match = FALSE;
|
|
|
|
f1 = l->data;
|
|
for (m = previous_files; m; m = m->next) {
|
|
f2 = m->data;
|
|
|
|
if (f1->sequence >= f2->sequence || g_str_equal (f1->uri, f2->uri)) {
|
|
match = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if (match)
|
|
break;
|
|
}
|
|
|
|
if (!l) {
|
|
/* No match, no sequence in the new playlist was higher than
|
|
* any in the old, and no URI was found again. This is bad! */
|
|
GST_ERROR ("Media sequences inconsistent, ignoring");
|
|
return FALSE;
|
|
} else {
|
|
g_assert (f1 != NULL);
|
|
g_assert (f2 != NULL);
|
|
|
|
for (; l && m; l = l->next, m = m->next) {
|
|
f1 = l->data;
|
|
f2 = m->data;
|
|
|
|
if (f1->sequence == f2->sequence) {
|
|
if (!g_str_equal (f1->uri, f2->uri)) {
|
|
/* Same sequence, different URI. This is bad! */
|
|
GST_ERROR ("Media sequences inconsistent, ignoring");
|
|
return FALSE;
|
|
} else {
|
|
/* Good case, we advance and check the next one */
|
|
}
|
|
} else if (g_str_equal (f1->uri, f2->uri)) {
|
|
/* Same URIs but different sequences, this is bad! */
|
|
GST_ERROR ("Media sequences inconsistent, ignoring");
|
|
return FALSE;
|
|
} else {
|
|
/* Not same URI, not same sequence but by construction sequence
|
|
* must be higher in the new one. All good in that case, if it
|
|
* isn't then this means that sequence numbers are decreasing
|
|
* or files were inserted */
|
|
if (f1->sequence < f2->sequence) {
|
|
GST_ERROR ("Media sequences inconsistent, ignoring");
|
|
return FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* All good if we're getting here */
|
|
}
|
|
} else {
|
|
GList *l, *m;
|
|
GstM3U8MediaFile *f1 = NULL, *f2 = NULL;
|
|
gint64 mediasequence;
|
|
|
|
for (l = self->files; l; l = l->next) {
|
|
gboolean match = FALSE;
|
|
|
|
f1 = l->data;
|
|
for (m = previous_files; m; m = m->next) {
|
|
f2 = m->data;
|
|
|
|
if (g_str_equal (f1->uri, f2->uri)) {
|
|
match = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (match)
|
|
break;
|
|
}
|
|
|
|
if (!l) {
|
|
/* No match, this means f2 is the last item in the previous playlist
|
|
* and we have to start our new playlist at that sequence */
|
|
mediasequence = f2->sequence + 1;
|
|
|
|
for (l = self->files; l; l = l->next) {
|
|
f1 = l->data;
|
|
f1->sequence = mediasequence;
|
|
mediasequence++;
|
|
}
|
|
} else {
|
|
/* Match, check that all following ones are matching too and continue
|
|
* sequence numbers from there on */
|
|
|
|
mediasequence = f2->sequence;
|
|
|
|
for (; l; l = l->next) {
|
|
f1 = l->data;
|
|
f2 = m ? m->data : NULL;
|
|
|
|
f1->sequence = mediasequence;
|
|
mediasequence++;
|
|
|
|
if (f2) {
|
|
if (!g_str_equal (f1->uri, f2->uri)) {
|
|
GST_WARNING ("Inconsistent URIs after playlist update");
|
|
}
|
|
}
|
|
|
|
if (m)
|
|
m = m->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*
|
|
* @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;
|
|
GList *previous_files = NULL;
|
|
gboolean have_mediasequence = FALSE;
|
|
|
|
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_WARNING ("Not a media playlist, but a master playlist!");
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
GST_TRACE ("data:\n%s", data);
|
|
|
|
g_free (self->last_data);
|
|
self->last_data = data;
|
|
|
|
self->current_file = NULL;
|
|
previous_files = 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;
|
|
have_mediasequence = TRUE;
|
|
}
|
|
} 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;
|
|
|
|
self->files = g_list_reverse (self->files);
|
|
|
|
if (previous_files) {
|
|
gboolean consistent = gst_m3u8_update_check_consistent_media_seqnums (self,
|
|
have_mediasequence, previous_files);
|
|
|
|
g_list_foreach (previous_files, (GFunc) gst_m3u8_media_file_unref, NULL);
|
|
g_list_free (previous_files);
|
|
previous_files = NULL;
|
|
|
|
/* error was reported above already */
|
|
if (!consistent)
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->files == NULL) {
|
|
GST_ERROR ("Invalid media playlist, it does not contain any media files");
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
/* calculate the start and end times of this media playlist. */
|
|
{
|
|
GList *walk;
|
|
GstM3U8MediaFile *file;
|
|
GstClockTime duration = 0;
|
|
|
|
mediasequence = -1;
|
|
|
|
for (walk = self->files; walk; walk = walk->next) {
|
|
file = walk->data;
|
|
|
|
if (mediasequence == -1) {
|
|
mediasequence = file->sequence;
|
|
} else if (mediasequence >= file->sequence) {
|
|
GST_ERROR ("Non-increasing media sequence");
|
|
GST_M3U8_UNLOCK (self);
|
|
return FALSE;
|
|
} else {
|
|
mediasequence = file->sequence;
|
|
}
|
|
|
|
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. Note
|
|
* the -1, because GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE = 1 means
|
|
* start 1 target-duration from the end */
|
|
for (i = 0; i < GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE - 1 && 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);
|
|
|
|
if (sequence_position)
|
|
*sequence_position = m3u8->sequence_position;
|
|
if (discont)
|
|
*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 = cur && ((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_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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
GstHLSMedia *
|
|
gst_hls_media_ref (GstHLSMedia * media)
|
|
{
|
|
g_assert (media != NULL && media->ref_count > 0);
|
|
g_atomic_int_add (&media->ref_count, 1);
|
|
return media;
|
|
}
|
|
|
|
void
|
|
gst_hls_media_unref (GstHLSMedia * media)
|
|
{
|
|
g_assert (media != NULL && media->ref_count > 0);
|
|
if (g_atomic_int_dec_and_test (&media->ref_count)) {
|
|
g_free (media->group_id);
|
|
g_free (media->name);
|
|
g_free (media->uri);
|
|
g_free (media);
|
|
}
|
|
}
|
|
|
|
static GstHLSMediaType
|
|
gst_m3u8_get_hls_media_type_from_string (const gchar * type_name)
|
|
{
|
|
if (strcmp (type_name, "AUDIO") == 0)
|
|
return GST_HLS_MEDIA_TYPE_AUDIO;
|
|
if (strcmp (type_name, "VIDEO") == 0)
|
|
return GST_HLS_MEDIA_TYPE_VIDEO;
|
|
if (strcmp (type_name, "SUBTITLES") == 0)
|
|
return GST_HLS_MEDIA_TYPE_SUBTITLES;
|
|
if (strcmp (type_name, "CLOSED_CAPTIONS") == 0)
|
|
return GST_HLS_MEDIA_TYPE_CLOSED_CAPTIONS;
|
|
|
|
return GST_HLS_MEDIA_TYPE_INVALID;
|
|
}
|
|
|
|
#define GST_HLS_MEDIA_TYPE_NAME(mtype) gst_m3u8_hls_media_type_get_nick(mtype)
|
|
static inline const gchar *
|
|
gst_m3u8_hls_media_type_get_nick (GstHLSMediaType mtype)
|
|
{
|
|
static const gchar *nicks[GST_HLS_N_MEDIA_TYPES] = { "audio", "video",
|
|
"subtitle", "closed-captions"
|
|
};
|
|
|
|
if (mtype < 0 || mtype >= GST_HLS_N_MEDIA_TYPES)
|
|
return "invalid";
|
|
|
|
return nicks[mtype];
|
|
}
|
|
|
|
/* returns unquoted copy of string */
|
|
static gchar *
|
|
gst_m3u8_unquote (const gchar * str)
|
|
{
|
|
const gchar *start, *end;
|
|
|
|
start = strchr (str, '"');
|
|
if (start == NULL)
|
|
return g_strdup (str);
|
|
end = strchr (start + 1, '"');
|
|
if (end == NULL) {
|
|
GST_WARNING ("Broken quoted string [%s] - can't find end quote", str);
|
|
return g_strdup (start + 1);
|
|
}
|
|
return g_strndup (start + 1, (gsize) (end - (start + 1)));
|
|
}
|
|
|
|
static GstHLSMedia *
|
|
gst_m3u8_parse_media (gchar * desc, const gchar * base_uri)
|
|
{
|
|
GstHLSMediaType mtype = GST_HLS_MEDIA_TYPE_INVALID;
|
|
GstHLSMedia *media;
|
|
gchar *a, *v;
|
|
|
|
media = g_new0 (GstHLSMedia, 1);
|
|
media->ref_count = 1;
|
|
media->playlist = gst_m3u8_new ();
|
|
|
|
GST_LOG ("parsing %s", desc);
|
|
while (desc != NULL && parse_attributes (&desc, &a, &v)) {
|
|
if (strcmp (a, "TYPE") == 0) {
|
|
media->mtype = gst_m3u8_get_hls_media_type_from_string (v);
|
|
} else if (strcmp (a, "GROUP-ID") == 0) {
|
|
g_free (media->group_id);
|
|
media->group_id = gst_m3u8_unquote (v);
|
|
} else if (strcmp (a, "NAME") == 0) {
|
|
g_free (media->name);
|
|
media->name = gst_m3u8_unquote (v);
|
|
} else if (strcmp (a, "URI") == 0) {
|
|
gchar *uri;
|
|
|
|
g_free (media->uri);
|
|
uri = gst_m3u8_unquote (v);
|
|
media->uri = uri_join (base_uri, uri);
|
|
g_free (uri);
|
|
} else if (strcmp (a, "LANGUAGE") == 0) {
|
|
g_free (media->lang);
|
|
media->lang = gst_m3u8_unquote (v);
|
|
} else if (strcmp (a, "DEFAULT") == 0) {
|
|
media->is_default = g_ascii_strcasecmp (v, "yes") == 0;
|
|
} else if (strcmp (a, "FORCED") == 0) {
|
|
media->forced = g_ascii_strcasecmp (v, "yes") == 0;
|
|
} else if (strcmp (a, "AUTOSELECT") == 0) {
|
|
media->autoselect = g_ascii_strcasecmp (v, "yes") == 0;
|
|
} else {
|
|
/* unhandled: ASSOC-LANGUAGE, INSTREAM-ID, CHARACTERISTICS */
|
|
GST_FIXME ("EXT-X-MEDIA: unhandled attribute: %s = %s", a, v);
|
|
}
|
|
}
|
|
|
|
if (media->mtype == GST_HLS_MEDIA_TYPE_INVALID)
|
|
goto required_attributes_missing;
|
|
|
|
if (media->uri == NULL)
|
|
goto existing_stream;
|
|
|
|
if (media->group_id == NULL || media->name == NULL)
|
|
goto required_attributes_missing;
|
|
|
|
if (mtype == GST_HLS_MEDIA_TYPE_CLOSED_CAPTIONS)
|
|
goto uri_with_cc;
|
|
|
|
GST_DEBUG ("media: %s, group '%s', name '%s', uri '%s', %s %s %s, lang=%s",
|
|
GST_HLS_MEDIA_TYPE_NAME (media->mtype), media->group_id, media->name,
|
|
media->uri, media->is_default ? "default" : "-",
|
|
media->autoselect ? "autoselect" : "-",
|
|
media->forced ? "forced" : "-", media->lang ? media->lang : "??");
|
|
|
|
return media;
|
|
|
|
uri_with_cc:
|
|
{
|
|
GST_WARNING ("closed captions EXT-X-MEDIA should not have URI specified");
|
|
goto out_error;
|
|
}
|
|
required_attributes_missing:
|
|
{
|
|
GST_WARNING ("EXT-X-MEDIA description is missing required attributes");
|
|
goto out_error;
|
|
/* fall through */
|
|
}
|
|
existing_stream:
|
|
{
|
|
GST_DEBUG ("EXT-X-MEDIA without URI, describes embedded stream, skipping");
|
|
/* fall through */
|
|
}
|
|
|
|
out_error:
|
|
{
|
|
gst_hls_media_unref (media);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static GstHLSVariantStream *
|
|
gst_hls_variant_stream_new (void)
|
|
{
|
|
GstHLSVariantStream *stream;
|
|
|
|
stream = g_new0 (GstHLSVariantStream, 1);
|
|
stream->m3u8 = gst_m3u8_new ();
|
|
stream->refcount = 1;
|
|
return stream;
|
|
}
|
|
|
|
GstHLSVariantStream *
|
|
gst_hls_variant_stream_ref (GstHLSVariantStream * stream)
|
|
{
|
|
g_atomic_int_inc (&stream->refcount);
|
|
return stream;
|
|
}
|
|
|
|
void
|
|
gst_hls_variant_stream_unref (GstHLSVariantStream * stream)
|
|
{
|
|
if (g_atomic_int_dec_and_test (&stream->refcount)) {
|
|
gint i;
|
|
|
|
g_free (stream->name);
|
|
g_free (stream->uri);
|
|
g_free (stream->codecs);
|
|
gst_m3u8_unref (stream->m3u8);
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
g_free (stream->media_groups[i]);
|
|
g_list_free_full (stream->media[i], (GDestroyNotify) gst_hls_media_unref);
|
|
}
|
|
g_free (stream);
|
|
}
|
|
}
|
|
|
|
static GstHLSVariantStream *
|
|
find_variant_stream_by_name (GList * list, const gchar * name)
|
|
{
|
|
for (; list != NULL; list = list->next) {
|
|
GstHLSVariantStream *variant_stream = list->data;
|
|
|
|
if (variant_stream->name != NULL && !strcmp (variant_stream->name, name))
|
|
return variant_stream;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static GstHLSVariantStream *
|
|
find_variant_stream_by_uri (GList * list, const gchar * uri)
|
|
{
|
|
for (; list != NULL; list = list->next) {
|
|
GstHLSVariantStream *variant_stream = list->data;
|
|
|
|
if (variant_stream->uri != NULL && !strcmp (variant_stream->uri, uri))
|
|
return variant_stream;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static GstHLSMasterPlaylist *
|
|
gst_hls_master_playlist_new (void)
|
|
{
|
|
GstHLSMasterPlaylist *playlist;
|
|
|
|
playlist = g_new0 (GstHLSMasterPlaylist, 1);
|
|
playlist->refcount = 1;
|
|
playlist->is_simple = FALSE;
|
|
|
|
return playlist;
|
|
}
|
|
|
|
void
|
|
gst_hls_master_playlist_unref (GstHLSMasterPlaylist * playlist)
|
|
{
|
|
if (g_atomic_int_dec_and_test (&playlist->refcount)) {
|
|
g_list_free_full (playlist->variants,
|
|
(GDestroyNotify) gst_hls_variant_stream_unref);
|
|
g_list_free_full (playlist->iframe_variants,
|
|
(GDestroyNotify) gst_hls_variant_stream_unref);
|
|
g_free (playlist->last_data);
|
|
g_free (playlist);
|
|
}
|
|
}
|
|
|
|
static gint
|
|
hls_media_name_compare_func (gconstpointer media, gconstpointer name)
|
|
{
|
|
return strcmp (((GstHLSMedia *) media)->name, (const gchar *) name);
|
|
}
|
|
|
|
/* Takes ownership of @data */
|
|
GstHLSMasterPlaylist *
|
|
gst_hls_master_playlist_new_from_data (gchar * data, const gchar * base_uri)
|
|
{
|
|
GHashTable *media_groups[GST_HLS_N_MEDIA_TYPES] = { NULL, };
|
|
GstHLSMasterPlaylist *playlist;
|
|
GstHLSVariantStream *pending_stream;
|
|
gchar *end, *free_data = data;
|
|
gint val, i;
|
|
GList *l;
|
|
|
|
if (!g_str_has_prefix (data, "#EXTM3U")) {
|
|
GST_WARNING ("Data doesn't start with #EXTM3U");
|
|
g_free (free_data);
|
|
return NULL;
|
|
}
|
|
|
|
playlist = gst_hls_master_playlist_new ();
|
|
|
|
/* store data before we modify it for parsing */
|
|
playlist->last_data = g_strdup (data);
|
|
|
|
GST_TRACE ("data:\n%s", data);
|
|
|
|
if (strstr (data, "\n#EXTINF:") != NULL) {
|
|
GST_INFO ("This is a simple media playlist, not a master playlist");
|
|
|
|
pending_stream = gst_hls_variant_stream_new ();
|
|
pending_stream->name = g_strdup (base_uri);
|
|
pending_stream->uri = g_strdup (base_uri);
|
|
gst_m3u8_set_uri (pending_stream->m3u8, base_uri, NULL, base_uri);
|
|
playlist->variants = g_list_append (playlist->variants, pending_stream);
|
|
playlist->default_variant = gst_hls_variant_stream_ref (pending_stream);
|
|
playlist->is_simple = TRUE;
|
|
|
|
if (!gst_m3u8_update (pending_stream->m3u8, data)) {
|
|
GST_WARNING ("Failed to parse media playlist");
|
|
gst_hls_master_playlist_unref (playlist);
|
|
playlist = NULL;
|
|
}
|
|
return playlist;
|
|
}
|
|
|
|
pending_stream = 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, *uri;
|
|
|
|
if (pending_stream == NULL) {
|
|
GST_LOG ("%s: got line without EXT-STREAM-INF, dropping", data);
|
|
goto next_line;
|
|
}
|
|
|
|
name = data;
|
|
uri = uri_join (base_uri, name);
|
|
if (uri == NULL)
|
|
goto next_line;
|
|
|
|
pending_stream->name = g_strdup (name);
|
|
pending_stream->uri = uri;
|
|
|
|
if (find_variant_stream_by_name (playlist->variants, name)
|
|
|| find_variant_stream_by_uri (playlist->variants, uri)) {
|
|
GST_DEBUG ("Already have a list with this name or URI: %s", name);
|
|
gst_hls_variant_stream_unref (pending_stream);
|
|
} else {
|
|
GST_INFO ("stream %s @ %u: %s", name, pending_stream->bandwidth, uri);
|
|
gst_m3u8_set_uri (pending_stream->m3u8, uri, NULL, name);
|
|
playlist->variants = g_list_append (playlist->variants, pending_stream);
|
|
/* use first stream in the playlist as default */
|
|
if (playlist->default_variant == NULL) {
|
|
playlist->default_variant =
|
|
gst_hls_variant_stream_ref (pending_stream);
|
|
}
|
|
}
|
|
pending_stream = NULL;
|
|
} else if (g_str_has_prefix (data, "#EXT-X-VERSION:")) {
|
|
if (int_from_string (data + 15, &data, &val))
|
|
playlist->version = val;
|
|
} else if (g_str_has_prefix (data, "#EXT-X-STREAM-INF:") ||
|
|
g_str_has_prefix (data, "#EXT-X-I-FRAME-STREAM-INF:")) {
|
|
GstHLSVariantStream *stream;
|
|
gchar *v, *a;
|
|
|
|
stream = gst_hls_variant_stream_new ();
|
|
stream->iframe = g_str_has_prefix (data, "#EXT-X-I-FRAME-STREAM-INF:");
|
|
data += stream->iframe ? 26 : 18;
|
|
while (data && parse_attributes (&data, &a, &v)) {
|
|
if (g_str_equal (a, "BANDWIDTH")) {
|
|
if (!int_from_string (v, NULL, &stream->bandwidth))
|
|
GST_WARNING ("Error while reading BANDWIDTH");
|
|
} else if (g_str_equal (a, "PROGRAM-ID")) {
|
|
if (!int_from_string (v, NULL, &stream->program_id))
|
|
GST_WARNING ("Error while reading PROGRAM-ID");
|
|
} else if (g_str_equal (a, "CODECS")) {
|
|
g_free (stream->codecs);
|
|
stream->codecs = g_strdup (v);
|
|
} else if (g_str_equal (a, "RESOLUTION")) {
|
|
if (!int_from_string (v, &v, &stream->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, &stream->height))
|
|
GST_WARNING ("Error while reading RESOLUTION height");
|
|
}
|
|
} else if (stream->iframe && g_str_equal (a, "URI")) {
|
|
stream->uri = uri_join (base_uri, v);
|
|
if (stream->uri != NULL) {
|
|
stream->name = g_strdup (stream->uri);
|
|
gst_m3u8_set_uri (stream->m3u8, stream->uri, NULL, stream->name);
|
|
} else {
|
|
gst_hls_variant_stream_unref (stream);
|
|
}
|
|
} else if (g_str_equal (a, "AUDIO")) {
|
|
g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_AUDIO]);
|
|
stream->media_groups[GST_HLS_MEDIA_TYPE_AUDIO] = gst_m3u8_unquote (v);
|
|
} else if (g_str_equal (a, "SUBTITLES")) {
|
|
g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_SUBTITLES]);
|
|
stream->media_groups[GST_HLS_MEDIA_TYPE_SUBTITLES] =
|
|
gst_m3u8_unquote (v);
|
|
} else if (g_str_equal (a, "VIDEO")) {
|
|
g_free (stream->media_groups[GST_HLS_MEDIA_TYPE_VIDEO]);
|
|
stream->media_groups[GST_HLS_MEDIA_TYPE_VIDEO] = gst_m3u8_unquote (v);
|
|
} else if (g_str_equal (a, "CLOSED-CAPTIONS")) {
|
|
/* closed captions will be embedded inside the video stream, ignore */
|
|
}
|
|
}
|
|
|
|
if (stream->iframe) {
|
|
if (find_variant_stream_by_uri (playlist->iframe_variants, stream->uri)) {
|
|
GST_DEBUG ("Already have a list with this URI");
|
|
gst_hls_variant_stream_unref (stream);
|
|
} else {
|
|
playlist->iframe_variants =
|
|
g_list_append (playlist->iframe_variants, stream);
|
|
}
|
|
} else {
|
|
if (pending_stream != NULL) {
|
|
GST_WARNING ("variant stream without uri, dropping");
|
|
gst_hls_variant_stream_unref (pending_stream);
|
|
}
|
|
pending_stream = stream;
|
|
}
|
|
} else if (g_str_has_prefix (data, "#EXT-X-MEDIA:")) {
|
|
GstHLSMedia *media;
|
|
GList *list;
|
|
|
|
media = gst_m3u8_parse_media (data + strlen ("#EXT-X-MEDIA:"), base_uri);
|
|
|
|
if (media == NULL)
|
|
goto next_line;
|
|
|
|
if (media_groups[media->mtype] == NULL) {
|
|
media_groups[media->mtype] =
|
|
g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
|
}
|
|
|
|
list = g_hash_table_lookup (media_groups[media->mtype], media->group_id);
|
|
|
|
/* make sure there isn't already a media with the same name */
|
|
if (!g_list_find_custom (list, media->name, hls_media_name_compare_func)) {
|
|
g_hash_table_replace (media_groups[media->mtype],
|
|
g_strdup (media->group_id), g_list_append (list, media));
|
|
GST_INFO ("Added media %s to group %s", media->name, media->group_id);
|
|
} else {
|
|
GST_WARNING (" media with name '%s' already exists in group '%s'!",
|
|
media->name, media->group_id);
|
|
gst_hls_media_unref (media);
|
|
}
|
|
} else if (*data != '\0') {
|
|
GST_LOG ("Ignored line: %s", data);
|
|
}
|
|
|
|
next_line:
|
|
if (!end)
|
|
break;
|
|
data = g_utf8_next_char (end); /* skip \n */
|
|
}
|
|
|
|
if (pending_stream != NULL) {
|
|
GST_WARNING ("#EXT-X-STREAM-INF without uri, dropping");
|
|
gst_hls_variant_stream_unref (pending_stream);
|
|
}
|
|
|
|
g_free (free_data);
|
|
|
|
/* Add alternative renditions media to variant streams */
|
|
for (l = playlist->variants; l != NULL; l = l->next) {
|
|
GstHLSVariantStream *stream = l->data;
|
|
GList *mlist;
|
|
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
if (stream->media_groups[i] != NULL && media_groups[i] != NULL) {
|
|
GST_INFO ("Adding %s group '%s' to stream '%s'",
|
|
GST_HLS_MEDIA_TYPE_NAME (i), stream->media_groups[i], stream->name);
|
|
|
|
mlist = g_hash_table_lookup (media_groups[i], stream->media_groups[i]);
|
|
|
|
if (mlist == NULL)
|
|
GST_WARNING ("Group '%s' does not exist!", stream->media_groups[i]);
|
|
|
|
while (mlist != NULL) {
|
|
GstHLSMedia *media = mlist->data;
|
|
|
|
GST_DEBUG (" %s media %s, uri: %s", GST_HLS_MEDIA_TYPE_NAME (i),
|
|
media->name, media->uri);
|
|
|
|
stream->media[i] =
|
|
g_list_append (stream->media[i], gst_hls_media_ref (media));
|
|
mlist = mlist->next;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* clean up our temporary alternative rendition groups hash tables */
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
if (media_groups[i] != NULL) {
|
|
GList *groups, *mlist;
|
|
|
|
groups = g_hash_table_get_keys (media_groups[i]);
|
|
for (l = groups; l != NULL; l = l->next) {
|
|
mlist = g_hash_table_lookup (media_groups[i], l->data);
|
|
g_list_free_full (mlist, (GDestroyNotify) gst_hls_media_unref);
|
|
}
|
|
g_list_free (groups);
|
|
g_hash_table_unref (media_groups[i]);
|
|
}
|
|
}
|
|
|
|
if (playlist->variants == NULL) {
|
|
GST_WARNING ("Master playlist without any media playlists!");
|
|
gst_hls_master_playlist_unref (playlist);
|
|
return NULL;
|
|
}
|
|
|
|
/* reorder variants by bitrate */
|
|
playlist->variants =
|
|
g_list_sort (playlist->variants,
|
|
(GCompareFunc) gst_hls_variant_stream_compare_by_bitrate);
|
|
|
|
playlist->iframe_variants =
|
|
g_list_sort (playlist->iframe_variants,
|
|
(GCompareFunc) gst_hls_variant_stream_compare_by_bitrate);
|
|
|
|
/* FIXME: restore old current_variant after master playlist update
|
|
* (move into code that does that update) */
|
|
#if 0
|
|
{
|
|
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;
|
|
}
|
|
|
|
/* here we sorted the lists */
|
|
|
|
if (iframe)
|
|
playlist->current_variant =
|
|
find_variant_stream_by_uri (playlist->iframe_variants,
|
|
top_variant_uri);
|
|
else
|
|
playlist->current_variant =
|
|
find_variant_stream_by_uri (playlist->variants, top_variant_uri);
|
|
}
|
|
#endif
|
|
|
|
GST_DEBUG ("parsed master playlist with %d streams and %d I-frame streams",
|
|
g_list_length (playlist->variants),
|
|
g_list_length (playlist->iframe_variants));
|
|
|
|
|
|
return playlist;
|
|
}
|
|
|
|
gboolean
|
|
gst_hls_variant_stream_is_live (GstHLSVariantStream * variant)
|
|
{
|
|
gboolean is_live;
|
|
|
|
g_return_val_if_fail (variant != NULL, FALSE);
|
|
|
|
is_live = gst_m3u8_is_live (variant->m3u8);
|
|
|
|
return is_live;
|
|
}
|
|
|
|
static gint
|
|
compare_media (const GstHLSMedia * a, const GstHLSMedia * b)
|
|
{
|
|
return strcmp (a->name, b->name);
|
|
}
|
|
|
|
GstHLSMedia *
|
|
gst_hls_variant_find_matching_media (GstHLSVariantStream * stream,
|
|
GstHLSMedia * media)
|
|
{
|
|
GList *mlist = stream->media[media->mtype];
|
|
GList *match;
|
|
|
|
if (mlist == NULL)
|
|
return NULL;
|
|
|
|
match = g_list_find_custom (mlist, media, (GCompareFunc) compare_media);
|
|
if (match == NULL)
|
|
return NULL;
|
|
|
|
return match->data;
|
|
}
|
|
|
|
GstHLSVariantStream *
|
|
gst_hls_master_playlist_get_variant_for_bitrate (GstHLSMasterPlaylist *
|
|
playlist, GstHLSVariantStream * current_variant, guint bitrate)
|
|
{
|
|
GstHLSVariantStream *variant = current_variant;
|
|
GList *l;
|
|
|
|
/* variant lists are sorted low to high, so iterate from highest to lowest */
|
|
if (current_variant == NULL || !current_variant->iframe)
|
|
l = g_list_last (playlist->variants);
|
|
else
|
|
l = g_list_last (playlist->iframe_variants);
|
|
|
|
while (l != NULL) {
|
|
variant = l->data;
|
|
if (variant->bandwidth <= bitrate)
|
|
break;
|
|
l = l->prev;
|
|
}
|
|
|
|
return variant;
|
|
}
|
|
|
|
GstHLSVariantStream *
|
|
gst_hls_master_playlist_get_matching_variant (GstHLSMasterPlaylist * playlist,
|
|
GstHLSVariantStream * current_variant)
|
|
{
|
|
if (current_variant->iframe) {
|
|
return find_variant_stream_by_uri (playlist->iframe_variants,
|
|
current_variant->uri);
|
|
}
|
|
|
|
return find_variant_stream_by_uri (playlist->variants, current_variant->uri);
|
|
}
|