mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-10 03:19:40 +00:00
87ab729551
If we have been updating too slowly and have gone out of the current live window, inform the baseclass accordingly. This is different from the case where we have been updating quicker than what the server provides. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/2679>
2287 lines
67 KiB
C
2287 lines
67 KiB
C
/* GStreamer
|
|
* Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
|
|
* Copyright (C) 2015 Tim-Philipp Müller <tim@centricular.com>
|
|
*
|
|
* Copyright (C) 2021-2022 Centricular Ltd
|
|
* Author: Edward Hervey <edward@centricular.com>
|
|
* Author: Jan Schmidt <jan@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 <gmodule.h>
|
|
#include <string.h>
|
|
|
|
#include <gst/pbutils/pbutils.h>
|
|
#include "m3u8.h"
|
|
#include "gstadaptivedemux.h"
|
|
#include "gsthlselements.h"
|
|
|
|
#define GST_CAT_DEFAULT hls2_debug
|
|
|
|
static void gst_m3u8_init_file_unref (GstM3U8InitFile * self);
|
|
static gchar *uri_join (const gchar * uri, const gchar * path);
|
|
|
|
GstHLSMediaPlaylist *
|
|
gst_hls_media_playlist_ref (GstHLSMediaPlaylist * m3u8)
|
|
{
|
|
g_assert (m3u8 != NULL && m3u8->ref_count > 0);
|
|
|
|
g_atomic_int_add (&m3u8->ref_count, 1);
|
|
return m3u8;
|
|
}
|
|
|
|
void
|
|
gst_hls_media_playlist_unref (GstHLSMediaPlaylist * 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_ptr_array_free (self->segments, TRUE);
|
|
|
|
g_free (self->last_data);
|
|
g_mutex_clear (&self->lock);
|
|
g_free (self);
|
|
}
|
|
}
|
|
|
|
static GstM3U8MediaSegment *
|
|
gst_m3u8_media_segment_new (gchar * uri, gchar * title, GstClockTime duration,
|
|
gint64 sequence, gint64 discont_sequence)
|
|
{
|
|
GstM3U8MediaSegment *file;
|
|
|
|
file = g_new0 (GstM3U8MediaSegment, 1);
|
|
file->uri = uri;
|
|
file->title = title;
|
|
file->duration = duration;
|
|
file->sequence = sequence;
|
|
file->discont_sequence = discont_sequence;
|
|
file->ref_count = 1;
|
|
|
|
file->stream_time = GST_CLOCK_STIME_NONE;
|
|
|
|
return file;
|
|
}
|
|
|
|
GstM3U8MediaSegment *
|
|
gst_m3u8_media_segment_ref (GstM3U8MediaSegment * mfile)
|
|
{
|
|
g_assert (mfile != NULL && mfile->ref_count > 0);
|
|
|
|
g_atomic_int_add (&mfile->ref_count, 1);
|
|
return mfile;
|
|
}
|
|
|
|
void
|
|
gst_m3u8_media_segment_unref (GstM3U8MediaSegment * self)
|
|
{
|
|
g_return_if_fail (self != NULL && self->ref_count > 0);
|
|
|
|
if (g_atomic_int_dec_and_test (&self->ref_count)) {
|
|
if (self->init_file)
|
|
gst_m3u8_init_file_unref (self->init_file);
|
|
g_free (self->title);
|
|
g_free (self->uri);
|
|
g_free (self->key);
|
|
if (self->datetime)
|
|
g_date_time_unref (self->datetime);
|
|
g_free (self);
|
|
}
|
|
}
|
|
|
|
static GstM3U8InitFile *
|
|
gst_m3u8_init_file_new (gchar * uri)
|
|
{
|
|
GstM3U8InitFile *file;
|
|
|
|
file = g_new0 (GstM3U8InitFile, 1);
|
|
file->uri = uri;
|
|
file->ref_count = 1;
|
|
|
|
return file;
|
|
}
|
|
|
|
static GstM3U8InitFile *
|
|
gst_m3u8_init_file_ref (GstM3U8InitFile * ifile)
|
|
{
|
|
g_assert (ifile != NULL && ifile->ref_count > 0);
|
|
|
|
g_atomic_int_add (&ifile->ref_count, 1);
|
|
return ifile;
|
|
}
|
|
|
|
static void
|
|
gst_m3u8_init_file_unref (GstM3U8InitFile * 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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
GstHLSMediaPlaylist *
|
|
gst_hls_media_playlist_new (const gchar * uri, const gchar * base_uri)
|
|
{
|
|
GstHLSMediaPlaylist *m3u8;
|
|
|
|
m3u8 = g_new0 (GstHLSMediaPlaylist, 1);
|
|
|
|
m3u8->uri = g_strdup (uri);
|
|
m3u8->base_uri = g_strdup (base_uri);
|
|
|
|
m3u8->version = 1;
|
|
m3u8->type = GST_HLS_PLAYLIST_TYPE_UNDEFINED;
|
|
m3u8->targetduration = GST_CLOCK_TIME_NONE;
|
|
m3u8->media_sequence = 0;
|
|
m3u8->discont_sequence = 0;
|
|
m3u8->endlist = FALSE;
|
|
m3u8->i_frame = FALSE;
|
|
m3u8->allowcache = TRUE;
|
|
|
|
m3u8->ext_x_key_present = FALSE;
|
|
m3u8->ext_x_pdt_present = FALSE;
|
|
|
|
m3u8->segments =
|
|
g_ptr_array_new_full (16, (GDestroyNotify) gst_m3u8_media_segment_unref);
|
|
|
|
m3u8->duration = 0;
|
|
|
|
g_mutex_init (&m3u8->lock);
|
|
m3u8->ref_count = 1;
|
|
|
|
return m3u8;
|
|
}
|
|
|
|
void
|
|
gst_hls_media_playlist_dump (GstHLSMediaPlaylist * self)
|
|
{
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
guint idx;
|
|
gchar *datestring;
|
|
|
|
GST_DEBUG ("uri : %s", self->uri);
|
|
GST_DEBUG ("base_uri : %s", self->base_uri);
|
|
|
|
GST_DEBUG ("version : %d", self->version);
|
|
|
|
GST_DEBUG ("targetduration : %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (self->targetduration));
|
|
GST_DEBUG ("media_sequence : %" G_GINT64_FORMAT, self->media_sequence);
|
|
GST_DEBUG ("discont_sequence : %" G_GINT64_FORMAT, self->discont_sequence);
|
|
|
|
GST_DEBUG ("endlist : %s",
|
|
self->endlist ? "present" : "NOT present");
|
|
GST_DEBUG ("i_frame : %s", self->i_frame ? "YES" : "NO");
|
|
|
|
GST_DEBUG ("EXT-X-KEY : %s",
|
|
self->ext_x_key_present ? "present" : "NOT present");
|
|
GST_DEBUG ("EXT-X-PROGRAM-DATE-TIME : %s",
|
|
self->ext_x_pdt_present ? "present" : "NOT present");
|
|
|
|
GST_DEBUG ("duration : %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (self->duration));
|
|
|
|
GST_DEBUG ("Segments : %d", self->segments->len);
|
|
for (idx = 0; idx < self->segments->len; idx++) {
|
|
GstM3U8MediaSegment *segment = g_ptr_array_index (self->segments, idx);
|
|
|
|
GST_DEBUG (" sequence:%" G_GINT64_FORMAT " discont_sequence:%"
|
|
G_GINT64_FORMAT, segment->sequence, segment->discont_sequence);
|
|
GST_DEBUG (" stream_time : %" GST_STIME_FORMAT,
|
|
GST_STIME_ARGS (segment->stream_time));
|
|
GST_DEBUG (" duration : %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (segment->duration));
|
|
if (segment->title)
|
|
GST_DEBUG (" title : %s", segment->title);
|
|
GST_DEBUG (" discont : %s", segment->discont ? "YES" : "NO");
|
|
if (segment->datetime) {
|
|
datestring = g_date_time_format_iso8601 (segment->datetime);
|
|
GST_DEBUG (" date/time : %s", datestring);
|
|
g_free (datestring);
|
|
}
|
|
GST_DEBUG (" uri : %s %" G_GUINT64_FORMAT " %" G_GINT64_FORMAT,
|
|
segment->uri, segment->offset, segment->size);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
gst_hls_media_playlist_postprocess_pdt (GstHLSMediaPlaylist * self)
|
|
{
|
|
gint idx, len = self->segments->len;
|
|
gint first_pdt = -1;
|
|
GstM3U8MediaSegment *previous = NULL;
|
|
GstM3U8MediaSegment *segment = NULL;
|
|
|
|
/* Iterate forward, and make sure datetimes are coherent */
|
|
for (idx = 0; idx < len; idx++, previous = segment) {
|
|
segment = g_ptr_array_index (self->segments, idx);
|
|
|
|
#define ABSDIFF(a,b) ((a) > (b) ? (a) - (b) : (b) - (a))
|
|
|
|
if (segment->datetime) {
|
|
if (first_pdt == -1)
|
|
first_pdt = idx;
|
|
if (!segment->discont && previous && previous->datetime) {
|
|
GstClockTimeDiff diff = g_date_time_difference (segment->datetime,
|
|
previous->datetime) * GST_USECOND;
|
|
if (ABSDIFF (diff, previous->duration) > 500 * GST_MSECOND) {
|
|
GST_LOG ("PDT diff %" GST_STIME_FORMAT " previous duration %"
|
|
GST_TIME_FORMAT, GST_STIME_ARGS (diff),
|
|
GST_TIME_ARGS (previous->duration));
|
|
g_date_time_unref (segment->datetime);
|
|
segment->datetime =
|
|
g_date_time_add (previous->datetime,
|
|
previous->duration / GST_USECOND);
|
|
}
|
|
}
|
|
} else {
|
|
if (segment->discont) {
|
|
GST_WARNING ("Discont segment doesn't have a PDT !");
|
|
} else if (previous) {
|
|
if (previous->datetime) {
|
|
segment->datetime =
|
|
g_date_time_add (previous->datetime,
|
|
previous->duration / GST_USECOND);
|
|
GST_LOG
|
|
("Generated new PDT based on previous segment PDT and duration");
|
|
} else {
|
|
GST_LOG ("Missing PDT, but can't generate it from previous one");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (first_pdt != -1 && first_pdt != 0) {
|
|
GST_LOG ("Scanning backwards from %d", first_pdt);
|
|
previous = g_ptr_array_index (self->segments, first_pdt);
|
|
for (idx = first_pdt - 1; idx >= 0; idx = idx - 1) {
|
|
GST_LOG ("%d", idx);
|
|
segment = g_ptr_array_index (self->segments, idx);
|
|
if (!segment->datetime && previous->datetime) {
|
|
segment->datetime =
|
|
g_date_time_add (previous->datetime,
|
|
-(segment->duration / GST_USECOND));
|
|
}
|
|
previous = segment;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Parse and create a new GstHLSMediaPlaylist */
|
|
GstHLSMediaPlaylist *
|
|
gst_hls_media_playlist_parse (gchar * data, const gchar * uri,
|
|
const gchar * base_uri)
|
|
{
|
|
gchar *input_data = data;
|
|
GstHLSMediaPlaylist *self;
|
|
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 = 0;
|
|
gint64 dsn = 0;
|
|
GDateTime *date_time = NULL;
|
|
GstM3U8InitFile *last_init_file = NULL;
|
|
GstM3U8MediaSegment *previous = NULL;
|
|
|
|
GST_LOG ("uri: %s", uri);
|
|
GST_LOG ("base_uri: %s", base_uri);
|
|
GST_TRACE ("data:\n%s", data);
|
|
|
|
if (!g_str_has_prefix (data, "#EXTM3U")) {
|
|
GST_WARNING ("Data doesn't start with #EXTM3U");
|
|
g_free (data);
|
|
return NULL;
|
|
}
|
|
|
|
if (g_strrstr (data, "\n#EXT-X-STREAM-INF:") != NULL) {
|
|
GST_WARNING ("Not a media playlist, but a master playlist!");
|
|
g_free (data);
|
|
return NULL;
|
|
}
|
|
|
|
self = gst_hls_media_playlist_new (uri, base_uri);
|
|
|
|
/* Store a copy of the data */
|
|
self->last_data = g_strdup (data);
|
|
|
|
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);
|
|
|
|
/* Let's check this is not a bogus duplicate entry */
|
|
if (previous && !discontinuity && !g_strcmp0 (data, previous->uri)
|
|
&& (offset == -1 || previous->offset == offset)) {
|
|
GST_WARNING ("Dropping duplicate segment entry");
|
|
g_free (data);
|
|
data = NULL;
|
|
date_time = NULL;
|
|
duration = 0;
|
|
title = NULL;
|
|
discontinuity = FALSE;
|
|
size = offset = -1;
|
|
goto next_line;
|
|
}
|
|
if (data != NULL) {
|
|
GstM3U8MediaSegment *file;
|
|
/* We can finally create the segment */
|
|
/* The disconinuity sequence number is only stored if the header has
|
|
* EXT-X-DISCONTINUITY-SEQUENCE present. */
|
|
file =
|
|
gst_m3u8_media_segment_new (data, title, duration, mediasequence++,
|
|
dsn);
|
|
self->duration += duration;
|
|
|
|
/* 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 {
|
|
file->offset = 0;
|
|
}
|
|
} else {
|
|
file->size = -1;
|
|
file->offset = 0;
|
|
}
|
|
|
|
file->datetime = date_time;
|
|
file->discont = discontinuity;
|
|
if (last_init_file)
|
|
file->init_file = gst_m3u8_init_file_ref (last_init_file);
|
|
|
|
date_time = NULL;
|
|
duration = 0;
|
|
title = NULL;
|
|
discontinuity = FALSE;
|
|
size = offset = -1;
|
|
g_ptr_array_add (self->segments, file);
|
|
previous = 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_DEBUG ("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, "PLAYLIST-TYPE:")) {
|
|
if (!g_strcmp0 (data + 21, "VOD"))
|
|
self->type = GST_HLS_PLAYLIST_TYPE_VOD;
|
|
else if (!g_strcmp0 (data + 21, "EVENT"))
|
|
self->type = GST_HLS_PLAYLIST_TYPE_EVENT;
|
|
else
|
|
GST_WARNING ("Unknown playlist type '%s'", data + 21);
|
|
} 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->media_sequence = mediasequence = val;
|
|
} else if (g_str_has_prefix (data_ext_x, "DISCONTINUITY-SEQUENCE:")) {
|
|
if (int_from_string (data + 30, &data, &val)
|
|
&& val != self->discont_sequence) {
|
|
dsn = self->discont_sequence = val;
|
|
self->has_ext_x_dsn = TRUE;
|
|
}
|
|
} else if (g_str_has_prefix (data_ext_x, "DISCONTINUITY")) {
|
|
dsn++;
|
|
discontinuity = TRUE;
|
|
} else if (g_str_has_prefix (data_ext_x, "PROGRAM-DATE-TIME:")) {
|
|
date_time = g_date_time_new_from_iso8601 (data + 25, NULL);
|
|
if (date_time)
|
|
self->ext_x_pdt_present = TRUE;
|
|
} 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") && !g_str_equal (v, "NONE")) {
|
|
GST_WARNING ("Encryption method %s not supported", v);
|
|
continue;
|
|
}
|
|
self->ext_x_key_present = TRUE;
|
|
}
|
|
}
|
|
} else if (g_str_has_prefix (data_ext_x, "BYTERANGE:")) {
|
|
gchar *v = data + 17;
|
|
|
|
size = -1;
|
|
offset = -1;
|
|
if (int64_from_string (v, &v, &size)) {
|
|
if (*v == '@' && !int64_from_string (v + 1, &v, &offset))
|
|
goto next_line;
|
|
/* */
|
|
if (offset == -1 && previous)
|
|
offset = previous->offset + previous->size;
|
|
} else {
|
|
goto next_line;
|
|
}
|
|
} else if (g_str_has_prefix (data_ext_x, "MAP:")) {
|
|
gchar *v, *a, *header_uri = NULL;
|
|
|
|
data = data + 11;
|
|
|
|
while (data != NULL && parse_attributes (&data, &a, &v)) {
|
|
if (strcmp (a, "URI") == 0) {
|
|
header_uri =
|
|
uri_join (self->base_uri ? self->base_uri : self->uri, v);
|
|
} else if (strcmp (a, "BYTERANGE") == 0) {
|
|
if (int64_from_string (v, &v, &size)) {
|
|
if (*v == '@' && !int64_from_string (v + 1, &v, &offset)) {
|
|
g_free (header_uri);
|
|
goto next_line;
|
|
}
|
|
} else {
|
|
g_free (header_uri);
|
|
goto next_line;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (header_uri) {
|
|
GstM3U8InitFile *init_file;
|
|
init_file = gst_m3u8_init_file_new (header_uri);
|
|
|
|
if (size != -1) {
|
|
init_file->size = size;
|
|
if (offset != -1)
|
|
init_file->offset = offset;
|
|
else
|
|
init_file->offset = 0;
|
|
} else {
|
|
init_file->size = -1;
|
|
init_file->offset = 0;
|
|
}
|
|
if (last_init_file)
|
|
gst_m3u8_init_file_unref (last_init_file);
|
|
|
|
last_init_file = init_file;
|
|
}
|
|
} else {
|
|
GST_LOG ("Ignored line: %s", data);
|
|
}
|
|
} else if (data[0]) {
|
|
/* Log non-empty lines */
|
|
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;
|
|
|
|
g_free (input_data);
|
|
|
|
if (last_init_file)
|
|
gst_m3u8_init_file_unref (last_init_file);
|
|
|
|
if (self->segments->len == 0) {
|
|
GST_ERROR ("Invalid media playlist, it does not contain any media files");
|
|
gst_hls_media_playlist_unref (self);
|
|
return NULL;
|
|
}
|
|
|
|
/* Now go over the parsed data to ensure MSN and/or PDT are set */
|
|
if (self->ext_x_pdt_present)
|
|
gst_hls_media_playlist_postprocess_pdt (self);
|
|
|
|
/* If we are not live, the stream time can be directly applied */
|
|
if (!GST_HLS_MEDIA_PLAYLIST_IS_LIVE (self)) {
|
|
gint iter, len = self->segments->len;
|
|
GstClockTimeDiff stream_time = 0;
|
|
|
|
for (iter = 0; iter < len; iter++) {
|
|
GstM3U8MediaSegment *segment = g_ptr_array_index (self->segments, iter);
|
|
segment->stream_time = stream_time;
|
|
stream_time += segment->duration;
|
|
}
|
|
}
|
|
|
|
gst_hls_media_playlist_dump (self);
|
|
return self;
|
|
}
|
|
|
|
/* Returns TRUE if the m3u8 as the same data as playlist_data */
|
|
gboolean
|
|
gst_hls_media_playlist_has_same_data (GstHLSMediaPlaylist * self,
|
|
gchar * playlist_data)
|
|
{
|
|
gboolean ret;
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_LOCK (self);
|
|
|
|
ret = self->last_data && g_str_equal (self->last_data, playlist_data);
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_UNLOCK (self);
|
|
|
|
return ret;
|
|
}
|
|
|
|
GstM3U8MediaSegment *
|
|
gst_hls_media_playlist_seek (GstHLSMediaPlaylist * playlist, gboolean forward,
|
|
GstSeekFlags flags, GstClockTimeDiff ts)
|
|
{
|
|
gboolean snap_nearest =
|
|
(flags & GST_SEEK_FLAG_SNAP_NEAREST) == GST_SEEK_FLAG_SNAP_NEAREST;
|
|
gboolean snap_after =
|
|
(flags & GST_SEEK_FLAG_SNAP_AFTER) == GST_SEEK_FLAG_SNAP_AFTER;
|
|
guint idx;
|
|
GstM3U8MediaSegment *res = NULL;
|
|
|
|
GST_DEBUG ("ts:%" GST_STIME_FORMAT " forward:%d playlist uri: %s",
|
|
GST_STIME_ARGS (ts), forward, playlist->uri);
|
|
|
|
for (idx = 0; idx < playlist->segments->len; idx++) {
|
|
GstM3U8MediaSegment *cand = g_ptr_array_index (playlist->segments, idx);
|
|
|
|
if ((forward & snap_after) || snap_nearest) {
|
|
if (cand->stream_time >= ts ||
|
|
(snap_nearest && (ts - cand->stream_time < cand->duration / 2))) {
|
|
res = cand;
|
|
goto out;
|
|
}
|
|
} else if (!forward && snap_after) {
|
|
GstClockTime next_pos = cand->stream_time + cand->duration;
|
|
|
|
if (next_pos <= ts && ts < next_pos + cand->duration) {
|
|
res = cand;
|
|
goto out;
|
|
}
|
|
} else if ((cand->stream_time <= ts || idx == 0)
|
|
&& ts < cand->stream_time + cand->duration) {
|
|
res = cand;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (res) {
|
|
GST_DEBUG ("Returning segment sn:%" G_GINT64_FORMAT " stream_time:%"
|
|
GST_STIME_FORMAT " duration:%" GST_TIME_FORMAT, res->sequence,
|
|
GST_STIME_ARGS (res->stream_time), GST_TIME_ARGS (res->duration));
|
|
gst_m3u8_media_segment_ref (res);
|
|
} else {
|
|
GST_DEBUG ("Couldn't find a match");
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/* Recalculate all segment DSN based on the DSN of the provided anchor segment
|
|
* (which must belong to the playlist). */
|
|
static void
|
|
gst_hls_media_playlist_recalculate_dsn (GstHLSMediaPlaylist * playlist,
|
|
GstM3U8MediaSegment * anchor)
|
|
{
|
|
guint idx;
|
|
gint iter;
|
|
GstM3U8MediaSegment *cand, *prev;
|
|
|
|
if (!g_ptr_array_find (playlist->segments, anchor, &idx)) {
|
|
g_assert (FALSE);
|
|
}
|
|
|
|
g_assert (idx != -1);
|
|
|
|
GST_DEBUG ("Re-calculating DSN from segment #%d %" G_GINT64_FORMAT,
|
|
idx, anchor->discont_sequence);
|
|
|
|
/* Forward */
|
|
prev = anchor;
|
|
for (iter = idx + 1; iter < playlist->segments->len; iter++) {
|
|
cand = g_ptr_array_index (playlist->segments, iter);
|
|
if (cand->discont)
|
|
cand->discont_sequence = prev->discont_sequence + 1;
|
|
else
|
|
cand->discont_sequence = prev->discont_sequence;
|
|
prev = cand;
|
|
}
|
|
|
|
/* Backward */
|
|
prev = anchor;
|
|
for (iter = idx - 1; iter >= 0; iter--) {
|
|
cand = g_ptr_array_index (playlist->segments, iter);
|
|
if (prev->discont)
|
|
cand->discont_sequence = prev->discont_sequence - 1;
|
|
else
|
|
cand->discont_sequence = prev->discont_sequence;
|
|
prev = cand;
|
|
}
|
|
}
|
|
|
|
|
|
/* Recalculate all segment stream time based on the stream time of the provided
|
|
* anchor segment (which must belong to the playlist) */
|
|
void
|
|
gst_hls_media_playlist_recalculate_stream_time (GstHLSMediaPlaylist * playlist,
|
|
GstM3U8MediaSegment * anchor)
|
|
{
|
|
guint idx;
|
|
gint iter;
|
|
GstM3U8MediaSegment *cand, *prev;
|
|
|
|
if (!g_ptr_array_find (playlist->segments, anchor, &idx)) {
|
|
g_assert (FALSE);
|
|
}
|
|
|
|
g_assert (GST_CLOCK_TIME_IS_VALID (anchor->stream_time));
|
|
g_assert (idx != -1);
|
|
|
|
GST_DEBUG ("Re-calculating stream times from segment #%d %" GST_TIME_FORMAT,
|
|
idx, GST_TIME_ARGS (anchor->stream_time));
|
|
|
|
/* Forward */
|
|
prev = anchor;
|
|
for (iter = idx + 1; iter < playlist->segments->len; iter++) {
|
|
cand = g_ptr_array_index (playlist->segments, iter);
|
|
cand->stream_time = prev->stream_time + prev->duration;
|
|
GST_DEBUG ("Forward iter %d %" GST_STIME_FORMAT, iter,
|
|
GST_STIME_ARGS (cand->stream_time));
|
|
prev = cand;
|
|
}
|
|
|
|
/* Backward */
|
|
prev = anchor;
|
|
for (iter = idx - 1; iter >= 0; iter--) {
|
|
cand = g_ptr_array_index (playlist->segments, iter);
|
|
cand->stream_time = prev->stream_time - cand->duration;
|
|
GST_DEBUG ("Backward iter %d %" GST_STIME_FORMAT, iter,
|
|
GST_STIME_ARGS (cand->stream_time));
|
|
prev = cand;
|
|
}
|
|
}
|
|
|
|
/* If a segment with the same URI, size, offset, SN and DSN is present in the
|
|
* playlist, returns that one */
|
|
static GstM3U8MediaSegment *
|
|
gst_hls_media_playlist_find_by_uri (GstHLSMediaPlaylist * playlist,
|
|
GstM3U8MediaSegment * segment)
|
|
{
|
|
guint idx;
|
|
|
|
for (idx = 0; idx < playlist->segments->len; idx++) {
|
|
GstM3U8MediaSegment *cand = g_ptr_array_index (playlist->segments, idx);
|
|
|
|
if (cand->sequence == segment->sequence &&
|
|
cand->discont_sequence == segment->discont_sequence &&
|
|
cand->offset == segment->offset && cand->size == segment->size &&
|
|
!g_strcmp0 (cand->uri, segment->uri)) {
|
|
return cand;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Find the equivalent segment in the given playlist.
|
|
*
|
|
* The returned segment does *NOT* have increased reference !
|
|
*/
|
|
static GstM3U8MediaSegment *
|
|
find_segment_in_playlist (GstHLSMediaPlaylist * playlist,
|
|
GstM3U8MediaSegment * segment)
|
|
{
|
|
GstM3U8MediaSegment *res = NULL;
|
|
guint idx;
|
|
|
|
/* The easy one. Happens when stream times need to be re-synced in an existing
|
|
* playlist */
|
|
if (g_ptr_array_find (playlist->segments, segment, NULL)) {
|
|
GST_DEBUG ("Present as-is in playlist");
|
|
return segment;
|
|
}
|
|
|
|
/* If there is an identical segment with the same URI and SN, use that one */
|
|
res = gst_hls_media_playlist_find_by_uri (playlist, segment);
|
|
if (res) {
|
|
GST_DEBUG ("Using same URI/DSN/SN match");
|
|
return res;
|
|
}
|
|
|
|
/* Try with PDT */
|
|
if (segment->datetime && playlist->ext_x_pdt_present) {
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
gchar *pdtstring = g_date_time_format_iso8601 (segment->datetime);
|
|
GST_DEBUG ("Search by datetime for %s", pdtstring);
|
|
g_free (pdtstring);
|
|
#endif
|
|
for (idx = 0; idx < playlist->segments->len; idx++) {
|
|
GstM3U8MediaSegment *cand = g_ptr_array_index (playlist->segments, idx);
|
|
|
|
if (idx == 0 && cand->datetime) {
|
|
/* Special case for segments which are just before the 1st one (within
|
|
* 20ms). We add another reference because it now also belongs to the
|
|
* current playlist */
|
|
GDateTime *seg_end = g_date_time_add (segment->datetime,
|
|
segment->duration / GST_USECOND);
|
|
GstClockTimeDiff ddiff =
|
|
g_date_time_difference (cand->datetime, seg_end) * GST_USECOND;
|
|
g_date_time_unref (seg_end);
|
|
if (ABS (ddiff) < 20 * GST_MSECOND) {
|
|
/* The reference segment ends within 20ms of the first segment, it is just before */
|
|
GST_DEBUG ("Reference segment ends within %" GST_STIME_FORMAT
|
|
" of first playlist segment, inserting before",
|
|
GST_STIME_ARGS (ddiff));
|
|
g_ptr_array_insert (playlist->segments, 0,
|
|
gst_m3u8_media_segment_ref (segment));
|
|
return segment;
|
|
}
|
|
if (ddiff > 0) {
|
|
/* If the reference segment is completely before the first segment, bail out */
|
|
GST_DEBUG ("Reference segment ends before first segment");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cand->datetime
|
|
&& g_date_time_difference (cand->datetime, segment->datetime) >= 0) {
|
|
GST_DEBUG ("Picking by date time");
|
|
return cand;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If not live, we can match by stream time */
|
|
if (!GST_HLS_MEDIA_PLAYLIST_IS_LIVE (playlist)) {
|
|
GST_DEBUG ("Search by Stream time for %" GST_STIME_FORMAT " duration:%"
|
|
GST_TIME_FORMAT, GST_STIME_ARGS (segment->stream_time),
|
|
GST_TIME_ARGS (segment->duration));
|
|
for (idx = 0; idx < playlist->segments->len; idx++) {
|
|
GstM3U8MediaSegment *cand = g_ptr_array_index (playlist->segments, idx);
|
|
|
|
/* If the candidate starts at or after the previous stream time */
|
|
if (cand->stream_time >= segment->stream_time) {
|
|
return cand;
|
|
}
|
|
|
|
/* If the previous end stream time is before the candidate end stream time */
|
|
if ((segment->stream_time + segment->duration) <
|
|
(cand->stream_time + cand->duration)) {
|
|
return cand;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Fallback with MSN */
|
|
GST_DEBUG ("Search by Media Sequence Number for sn:%" G_GINT64_FORMAT " dsn:%"
|
|
G_GINT64_FORMAT, segment->sequence, segment->discont_sequence);
|
|
for (idx = 0; idx < playlist->segments->len; idx++) {
|
|
GstM3U8MediaSegment *cand = g_ptr_array_index (playlist->segments, idx);
|
|
|
|
if (idx == 0 && cand->sequence == segment->sequence + 1) {
|
|
/* Special case for segments just before the 1st one. We add another
|
|
* reference because it now also belongs to the current playlist */
|
|
GST_DEBUG ("reference segment is just before 1st segment, inserting");
|
|
g_ptr_array_insert (playlist->segments, 0,
|
|
gst_m3u8_media_segment_ref (segment));
|
|
return segment;
|
|
}
|
|
|
|
if ((segment->discont_sequence == cand->discont_sequence
|
|
|| !playlist->has_ext_x_dsn)
|
|
&& (cand->sequence == segment->sequence)) {
|
|
return cand;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Given a media segment (potentially from another media playlist), find the
|
|
* equivalent media segment in this playlist.
|
|
*
|
|
* This will also recalculate all stream times based on that segment stream
|
|
* time (i.e. "sync" the playlist to that previous time).
|
|
*
|
|
* If an equivalent/identical one is found it is returned with
|
|
* the reference count incremented
|
|
*
|
|
* If the reference segment is *just* before the 1st segment in the playlist, it
|
|
* will be inserted and returned. This allows coping with non-overlapping (but
|
|
* contiguous) playlist updates.
|
|
*/
|
|
GstM3U8MediaSegment *
|
|
gst_hls_media_playlist_sync_to_segment (GstHLSMediaPlaylist * playlist,
|
|
GstM3U8MediaSegment * segment)
|
|
{
|
|
GstM3U8MediaSegment *res = NULL;
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
gchar *pdtstring;
|
|
#endif
|
|
|
|
g_return_val_if_fail (playlist, NULL);
|
|
g_return_val_if_fail (segment, NULL);
|
|
|
|
GST_DEBUG ("Re-syncing to segment %" GST_STIME_FORMAT " duration:%"
|
|
GST_TIME_FORMAT " sn:%" G_GINT64_FORMAT "/dsn:%" G_GINT64_FORMAT
|
|
" uri:%s in playlist %s", GST_STIME_ARGS (segment->stream_time),
|
|
GST_TIME_ARGS (segment->duration), segment->sequence,
|
|
segment->discont_sequence, segment->uri, playlist->uri);
|
|
|
|
res = find_segment_in_playlist (playlist, segment);
|
|
|
|
/* For live playlists we re-calculate all stream times based on the existing
|
|
* stream time. Non-live playlists have their stream time calculated at
|
|
* parsing time. */
|
|
if (res) {
|
|
gst_m3u8_media_segment_ref (res);
|
|
if (res->stream_time == GST_CLOCK_STIME_NONE)
|
|
res->stream_time = segment->stream_time;
|
|
if (GST_HLS_MEDIA_PLAYLIST_IS_LIVE (playlist))
|
|
gst_hls_media_playlist_recalculate_stream_time (playlist, res);
|
|
/* If the playlist didn't specify a reference discont sequence number, we
|
|
* carry over the one from the reference segment */
|
|
if (!playlist->has_ext_x_dsn
|
|
&& res->discont_sequence != segment->discont_sequence) {
|
|
res->discont_sequence = segment->discont_sequence;
|
|
gst_hls_media_playlist_recalculate_dsn (playlist, res);
|
|
}
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
pdtstring =
|
|
res->datetime ? g_date_time_format_iso8601 (res->datetime) : NULL;
|
|
GST_DEBUG ("Returning segment sn:%" G_GINT64_FORMAT " dsn:%" G_GINT64_FORMAT
|
|
" stream_time:%" GST_STIME_FORMAT " duration:%" GST_TIME_FORMAT
|
|
" datetime:%s", res->sequence, res->discont_sequence,
|
|
GST_STIME_ARGS (res->stream_time), GST_TIME_ARGS (res->duration),
|
|
pdtstring);
|
|
g_free (pdtstring);
|
|
#endif
|
|
} else if (!GST_HLS_MEDIA_PLAYLIST_IS_LIVE (playlist)) {
|
|
GST_DEBUG ("Could not find a match");
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
GstM3U8MediaSegment *
|
|
gst_hls_media_playlist_get_starting_segment (GstHLSMediaPlaylist * self)
|
|
{
|
|
GstM3U8MediaSegment *res;
|
|
|
|
GST_DEBUG ("playlist %s", self->uri);
|
|
|
|
if (!GST_HLS_MEDIA_PLAYLIST_IS_LIVE (self)) {
|
|
/* For non-live, we just grab the first one */
|
|
res = g_ptr_array_index (self->segments, 0);
|
|
} else {
|
|
/* Live playlist */
|
|
res =
|
|
g_ptr_array_index (self->segments,
|
|
MAX ((gint) self->segments->len - GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE -
|
|
1, 0));
|
|
}
|
|
|
|
if (res) {
|
|
GST_DEBUG ("Using segment sn:%" G_GINT64_FORMAT " dsn:%" G_GINT64_FORMAT,
|
|
res->sequence, res->discont_sequence);
|
|
gst_m3u8_media_segment_ref (res);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/* Calls this to carry over stream time, DSN, ... from one playlist to another.
|
|
*
|
|
* This should be used when a reference media segment couldn't be matched in the
|
|
* playlist, but we still want to carry over the information from a reference
|
|
* playlist to an updated one. This can happen with live playlists where the
|
|
* reference media segment is no longer present but the playlists intersect */
|
|
gboolean
|
|
gst_hls_media_playlist_sync_to_playlist (GstHLSMediaPlaylist * playlist,
|
|
GstHLSMediaPlaylist * reference)
|
|
{
|
|
GstM3U8MediaSegment *res = NULL;
|
|
GstM3U8MediaSegment *cand = NULL;
|
|
guint idx;
|
|
|
|
g_return_val_if_fail (playlist && reference, FALSE);
|
|
|
|
/* The new playlist is supposed to be an update of the reference playlist,
|
|
* therefore we will try from the last segment of the reference playlist and
|
|
* go backwards */
|
|
for (idx = reference->segments->len - 1; idx; idx--) {
|
|
cand = g_ptr_array_index (reference->segments, idx);
|
|
res = find_segment_in_playlist (playlist, cand);
|
|
if (res)
|
|
break;
|
|
}
|
|
|
|
if (res == NULL) {
|
|
GST_WARNING ("Could not synchronize media playlists");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Carry over reference stream time */
|
|
if (res->stream_time == GST_CLOCK_STIME_NONE)
|
|
res->stream_time = cand->stream_time;
|
|
if (GST_HLS_MEDIA_PLAYLIST_IS_LIVE (playlist))
|
|
gst_hls_media_playlist_recalculate_stream_time (playlist, res);
|
|
/* If the playlist didn't specify a reference discont sequence number, we
|
|
* carry over the one from the reference segment */
|
|
if (!playlist->has_ext_x_dsn
|
|
&& res->discont_sequence != cand->discont_sequence) {
|
|
res->discont_sequence = cand->discont_sequence;
|
|
gst_hls_media_playlist_recalculate_dsn (playlist, res);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
gst_hls_media_playlist_has_next_fragment (GstHLSMediaPlaylist * m3u8,
|
|
GstM3U8MediaSegment * current, gboolean forward)
|
|
{
|
|
guint idx;
|
|
gboolean have_next = TRUE;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, FALSE);
|
|
g_return_val_if_fail (current != NULL, FALSE);
|
|
|
|
GST_DEBUG ("playlist %s", m3u8->uri);
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_LOCK (m3u8);
|
|
|
|
if (!g_ptr_array_find (m3u8->segments, current, &idx))
|
|
have_next = FALSE;
|
|
else if (idx == 0 && !forward)
|
|
have_next = FALSE;
|
|
else if (forward && idx == (m3u8->segments->len - 1))
|
|
have_next = FALSE;
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_UNLOCK (m3u8);
|
|
|
|
GST_DEBUG ("Returning %d", have_next);
|
|
|
|
return have_next;
|
|
}
|
|
|
|
|
|
GstM3U8MediaSegment *
|
|
gst_hls_media_playlist_advance_fragment (GstHLSMediaPlaylist * m3u8,
|
|
GstM3U8MediaSegment * current, gboolean forward)
|
|
{
|
|
GstM3U8MediaSegment *file = NULL;
|
|
guint idx;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, NULL);
|
|
g_return_val_if_fail (current != NULL, NULL);
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_LOCK (m3u8);
|
|
|
|
GST_DEBUG ("playlist %s", m3u8->uri);
|
|
|
|
if (m3u8->segments->len < 2) {
|
|
GST_DEBUG ("Playlist only contains one fragment, can't advance");
|
|
goto out;
|
|
}
|
|
|
|
if (!g_ptr_array_find (m3u8->segments, current, &idx)) {
|
|
GST_ERROR ("Requested to advance froma fragment not present in playlist");
|
|
goto out;
|
|
}
|
|
|
|
if (forward && idx < (m3u8->segments->len - 1)) {
|
|
file =
|
|
gst_m3u8_media_segment_ref (g_ptr_array_index (m3u8->segments,
|
|
idx + 1));
|
|
} else if (!forward && idx > 0) {
|
|
file =
|
|
gst_m3u8_media_segment_ref (g_ptr_array_index (m3u8->segments,
|
|
idx - 1));
|
|
}
|
|
|
|
if (file)
|
|
GST_DEBUG ("Advanced to segment sn:%" G_GINT64_FORMAT " dsn:%"
|
|
G_GINT64_FORMAT, file->sequence, file->discont_sequence);
|
|
else
|
|
GST_DEBUG ("Could not find %s fragment", forward ? "next" : "previous");
|
|
|
|
out:
|
|
GST_HLS_MEDIA_PLAYLIST_UNLOCK (m3u8);
|
|
|
|
return file;
|
|
}
|
|
|
|
GstClockTime
|
|
gst_hls_media_playlist_get_duration (GstHLSMediaPlaylist * m3u8)
|
|
{
|
|
GstClockTime duration = GST_CLOCK_TIME_NONE;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, GST_CLOCK_TIME_NONE);
|
|
|
|
GST_DEBUG ("playlist %s", m3u8->uri);
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_LOCK (m3u8);
|
|
/* We can only get the duration for on-demand streams */
|
|
if (m3u8->endlist) {
|
|
if (m3u8->segments->len) {
|
|
GstM3U8MediaSegment *first = g_ptr_array_index (m3u8->segments, 0);
|
|
GstM3U8MediaSegment *last =
|
|
g_ptr_array_index (m3u8->segments, m3u8->segments->len - 1);
|
|
duration = last->stream_time + last->duration - first->stream_time;
|
|
if (duration != m3u8->duration)
|
|
GST_ERROR ("difference in calculated duration ? %" GST_TIME_FORMAT
|
|
" vs %" GST_TIME_FORMAT, GST_TIME_ARGS (duration),
|
|
GST_TIME_ARGS (m3u8->duration));
|
|
}
|
|
duration = m3u8->duration;
|
|
}
|
|
GST_HLS_MEDIA_PLAYLIST_UNLOCK (m3u8);
|
|
|
|
GST_DEBUG ("duration %" GST_TIME_FORMAT, GST_TIME_ARGS (duration));
|
|
|
|
return duration;
|
|
}
|
|
|
|
gchar *
|
|
gst_hls_media_playlist_get_uri (GstHLSMediaPlaylist * m3u8)
|
|
{
|
|
gchar *uri;
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_LOCK (m3u8);
|
|
uri = g_strdup (m3u8->uri);
|
|
GST_HLS_MEDIA_PLAYLIST_UNLOCK (m3u8);
|
|
|
|
return uri;
|
|
}
|
|
|
|
gboolean
|
|
gst_hls_media_playlist_is_live (GstHLSMediaPlaylist * m3u8)
|
|
{
|
|
gboolean is_live;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, FALSE);
|
|
|
|
GST_HLS_MEDIA_PLAYLIST_LOCK (m3u8);
|
|
is_live = GST_HLS_MEDIA_PLAYLIST_IS_LIVE (m3u8);
|
|
GST_HLS_MEDIA_PLAYLIST_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)
|
|
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)
|
|
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);
|
|
if (!ret)
|
|
GST_WARNING ("Can't build a valid uri from '%s' '%s'", uri1, uri2);
|
|
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
gst_hls_media_playlist_has_lost_sync (GstHLSMediaPlaylist * m3u8,
|
|
GstClockTime position)
|
|
{
|
|
GstM3U8MediaSegment *first;
|
|
|
|
if (m3u8->segments->len < 1)
|
|
return TRUE;
|
|
first = g_ptr_array_index (m3u8->segments, 0);
|
|
|
|
GST_DEBUG ("position %" GST_TIME_FORMAT " first %" GST_STIME_FORMAT
|
|
" duration %" GST_STIME_FORMAT, GST_TIME_ARGS (position),
|
|
GST_STIME_ARGS (first->stream_time), GST_STIME_ARGS (first->duration));
|
|
|
|
if (first->stream_time <= 0)
|
|
return FALSE;
|
|
|
|
/* If we're definitely before the first fragment, we lost sync */
|
|
if ((position + (first->duration / 2)) < first->stream_time)
|
|
return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
gst_hls_media_playlist_get_seek_range (GstHLSMediaPlaylist * m3u8,
|
|
gint64 * start, gint64 * stop)
|
|
{
|
|
GstM3U8MediaSegment *first, *last;
|
|
guint min_distance = 1;
|
|
|
|
g_return_val_if_fail (m3u8 != NULL, FALSE);
|
|
|
|
if (m3u8->segments->len < 1)
|
|
return FALSE;
|
|
|
|
first = g_ptr_array_index (m3u8->segments, 0);
|
|
*start = first->stream_time;
|
|
|
|
if (GST_HLS_MEDIA_PLAYLIST_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 =
|
|
MIN (GST_M3U8_LIVE_MIN_FRAGMENT_DISTANCE, m3u8->segments->len - 1);
|
|
}
|
|
|
|
last = g_ptr_array_index (m3u8->segments, m3u8->segments->len - min_distance);
|
|
*stop = last->stream_time + last->duration;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
GstHLSRenditionStream *
|
|
gst_hls_rendition_stream_ref (GstHLSRenditionStream * media)
|
|
{
|
|
g_assert (media != NULL && media->ref_count > 0);
|
|
g_atomic_int_add (&media->ref_count, 1);
|
|
return media;
|
|
}
|
|
|
|
void
|
|
gst_hls_rendition_stream_unref (GstHLSRenditionStream * 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->lang);
|
|
g_free (media);
|
|
}
|
|
}
|
|
|
|
static GstHLSRenditionStreamType
|
|
gst_m3u8_get_hls_media_type_from_string (const gchar * type_name)
|
|
{
|
|
if (strcmp (type_name, "AUDIO") == 0)
|
|
return GST_HLS_RENDITION_STREAM_TYPE_AUDIO;
|
|
if (strcmp (type_name, "VIDEO") == 0)
|
|
return GST_HLS_RENDITION_STREAM_TYPE_VIDEO;
|
|
if (strcmp (type_name, "SUBTITLES") == 0)
|
|
return GST_HLS_RENDITION_STREAM_TYPE_SUBTITLES;
|
|
if (strcmp (type_name, "CLOSED_CAPTIONS") == 0)
|
|
return GST_HLS_RENDITION_STREAM_TYPE_CLOSED_CAPTIONS;
|
|
|
|
return GST_HLS_RENDITION_STREAM_TYPE_INVALID;
|
|
}
|
|
|
|
#define GST_HLS_RENDITION_STREAM_TYPE_NAME(mtype) gst_hls_rendition_stream_type_get_name(mtype)
|
|
const gchar *
|
|
gst_hls_rendition_stream_type_get_name (GstHLSRenditionStreamType 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 GstHLSRenditionStream *
|
|
gst_m3u8_parse_media (gchar * desc, const gchar * base_uri)
|
|
{
|
|
GstHLSRenditionStream *media;
|
|
gchar *a, *v;
|
|
|
|
media = g_new0 (GstHLSRenditionStream, 1);
|
|
media->ref_count = 1;
|
|
media->mtype = GST_HLS_RENDITION_STREAM_TYPE_INVALID;
|
|
|
|
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_RENDITION_STREAM_TYPE_INVALID)
|
|
goto required_attributes_missing;
|
|
|
|
if (media->group_id == NULL || media->name == NULL)
|
|
goto required_attributes_missing;
|
|
|
|
if (media->mtype == GST_HLS_RENDITION_STREAM_TYPE_CLOSED_CAPTIONS)
|
|
goto uri_with_cc;
|
|
|
|
GST_DEBUG ("media: %s, group '%s', name '%s', uri '%s', %s %s %s, lang=%s",
|
|
GST_HLS_RENDITION_STREAM_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;
|
|
}
|
|
|
|
out_error:
|
|
{
|
|
gst_hls_rendition_stream_unref (media);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
GstStreamType
|
|
gst_hls_get_stream_type_from_structure (GstStructure * st)
|
|
{
|
|
const gchar *name = gst_structure_get_name (st);
|
|
|
|
if (g_str_has_prefix (name, "audio/"))
|
|
return GST_STREAM_TYPE_AUDIO;
|
|
|
|
if (g_str_has_prefix (name, "video/"))
|
|
return GST_STREAM_TYPE_VIDEO;
|
|
|
|
if (g_str_has_prefix (name, "application/x-subtitle"))
|
|
return GST_STREAM_TYPE_TEXT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
GstStreamType
|
|
gst_hls_get_stream_type_from_caps (GstCaps * caps)
|
|
{
|
|
GstStreamType ret = 0;
|
|
guint i, nb;
|
|
nb = gst_caps_get_size (caps);
|
|
for (i = 0; i < nb; i++) {
|
|
GstStructure *cand = gst_caps_get_structure (caps, i);
|
|
|
|
ret |= gst_hls_get_stream_type_from_structure (cand);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static GstHLSVariantStream *
|
|
gst_hls_variant_stream_new (void)
|
|
{
|
|
GstHLSVariantStream *stream;
|
|
|
|
stream = g_new0 (GstHLSVariantStream, 1);
|
|
stream->refcount = 1;
|
|
stream->codecs_stream_type = 0;
|
|
return stream;
|
|
}
|
|
|
|
GstHLSVariantStream *
|
|
hls_variant_stream_ref (GstHLSVariantStream * stream)
|
|
{
|
|
g_atomic_int_inc (&stream->refcount);
|
|
return stream;
|
|
}
|
|
|
|
void
|
|
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);
|
|
if (stream->caps)
|
|
gst_caps_unref (stream->caps);
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
g_free (stream->media_groups[i]);
|
|
}
|
|
g_list_free_full (stream->fallback, g_free);
|
|
g_free (stream);
|
|
}
|
|
}
|
|
|
|
static GstHLSVariantStream *
|
|
gst_hls_variant_parse (gchar * data, const gchar * base_uri)
|
|
{
|
|
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 (!stream->bandwidth) {
|
|
if (!int_from_string (v, NULL, &stream->bandwidth))
|
|
GST_WARNING ("Error while reading BANDWIDTH");
|
|
}
|
|
} else if (g_str_equal (a, "AVERAGE-BANDWIDTH")) {
|
|
GST_DEBUG
|
|
("AVERAGE-BANDWIDTH attribute available. Using it as stream bandwidth");
|
|
if (!int_from_string (v, NULL, &stream->bandwidth))
|
|
GST_WARNING ("Error while reading AVERAGE-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);
|
|
stream->caps = gst_codec_utils_caps_from_mime_codec (stream->codecs);
|
|
stream->codecs_stream_type =
|
|
gst_hls_get_stream_type_from_caps (stream->caps);
|
|
} 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);
|
|
} else if (g_str_equal (a, "AUDIO")) {
|
|
g_free (stream->media_groups[GST_HLS_RENDITION_STREAM_TYPE_AUDIO]);
|
|
stream->media_groups[GST_HLS_RENDITION_STREAM_TYPE_AUDIO] =
|
|
gst_m3u8_unquote (v);
|
|
} else if (g_str_equal (a, "SUBTITLES")) {
|
|
g_free (stream->media_groups[GST_HLS_RENDITION_STREAM_TYPE_SUBTITLES]);
|
|
stream->media_groups[GST_HLS_RENDITION_STREAM_TYPE_SUBTITLES] =
|
|
gst_m3u8_unquote (v);
|
|
} else if (g_str_equal (a, "VIDEO")) {
|
|
g_free (stream->media_groups[GST_HLS_RENDITION_STREAM_TYPE_VIDEO]);
|
|
stream->media_groups[GST_HLS_RENDITION_STREAM_TYPE_VIDEO] =
|
|
gst_m3u8_unquote (v);
|
|
} else if (g_str_equal (a, "CLOSED-CAPTIONS")) {
|
|
/* closed captions will be embedded inside the video stream, ignore */
|
|
}
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
static gchar *
|
|
generate_variant_stream_name (gchar * uri, gint bandwidth)
|
|
{
|
|
gchar *checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA1, uri, -1);
|
|
gchar *res = g_strdup_printf ("variant-%dbps-%s", bandwidth, checksum);
|
|
|
|
g_free (checksum);
|
|
return res;
|
|
}
|
|
|
|
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 GstHLSVariantStream *
|
|
find_variant_stream_for_fallback (GList * list, GstHLSVariantStream * fallback)
|
|
{
|
|
for (; list != NULL; list = list->next) {
|
|
GstHLSVariantStream *variant_stream = list->data;
|
|
|
|
if (variant_stream->bandwidth == fallback->bandwidth &&
|
|
variant_stream->width == fallback->width &&
|
|
variant_stream->height == fallback->height &&
|
|
variant_stream->iframe == fallback->iframe &&
|
|
!g_strcmp0 (variant_stream->codecs, fallback->codecs))
|
|
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
|
|
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);
|
|
if (playlist->default_variant)
|
|
gst_hls_variant_stream_unref (playlist->default_variant);
|
|
g_free (playlist->last_data);
|
|
g_free (playlist);
|
|
}
|
|
}
|
|
|
|
static gint
|
|
hls_media_compare_func (GstHLSRenditionStream * ma, GstHLSRenditionStream * mb)
|
|
{
|
|
if (ma->mtype != mb->mtype)
|
|
return ma->mtype - mb->mtype;
|
|
|
|
return strcmp (ma->name, mb->name) || strcmp (ma->group_id, mb->group_id);
|
|
}
|
|
|
|
static GstCaps *
|
|
stream_get_media_caps (GstHLSVariantStream * stream,
|
|
GstHLSRenditionStreamType mtype)
|
|
{
|
|
GstStructure *st = NULL;
|
|
GstCaps *ret;
|
|
guint i, nb;
|
|
|
|
if (stream->caps == NULL)
|
|
return NULL;
|
|
|
|
nb = gst_caps_get_size (stream->caps);
|
|
for (i = 0; i < nb; i++) {
|
|
GstStructure *cand = gst_caps_get_structure (stream->caps, i);
|
|
const gchar *name = gst_structure_get_name (cand);
|
|
gboolean matched;
|
|
|
|
switch (mtype) {
|
|
case GST_HLS_RENDITION_STREAM_TYPE_AUDIO:
|
|
matched = g_str_has_prefix (name, "audio/");
|
|
break;
|
|
case GST_HLS_RENDITION_STREAM_TYPE_VIDEO:
|
|
matched = g_str_has_prefix (name, "video/");
|
|
break;
|
|
case GST_HLS_RENDITION_STREAM_TYPE_SUBTITLES:
|
|
matched = g_str_has_prefix (name, "application/x-subtitle");
|
|
break;
|
|
default:
|
|
matched = FALSE;
|
|
break;
|
|
}
|
|
|
|
if (!matched)
|
|
continue;
|
|
|
|
if (st) {
|
|
GST_WARNING ("More than one caps for the same type, can't match");
|
|
return NULL;
|
|
}
|
|
|
|
st = cand;
|
|
}
|
|
|
|
if (!st)
|
|
return NULL;
|
|
|
|
ret = gst_caps_new_empty ();
|
|
gst_caps_append_structure (ret, gst_structure_copy (st));
|
|
return ret;
|
|
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* gst_hls_master_playlist_new_from_data:
|
|
* @data: (transfer full): The manifest to parse
|
|
* @base_uri: The URI of the manifest
|
|
*
|
|
* Parse the provided manifest and construct the master playlist.
|
|
*
|
|
* Returns: The parse GstHLSMasterPlaylist , or NULL if there was an error.
|
|
*/
|
|
GstHLSMasterPlaylist *
|
|
hls_master_playlist_new_from_data (gchar * data, const gchar * base_uri)
|
|
{
|
|
GstHLSMasterPlaylist *playlist;
|
|
GstHLSVariantStream *pending_stream, *existing_stream;
|
|
gchar *end, *free_data = data;
|
|
gint val;
|
|
GList *tmp;
|
|
GstStreamType most_seen_types = 0;
|
|
|
|
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);
|
|
|
|
/* Detect early whether this manifest describes a simple media playlist or
|
|
* not */
|
|
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 ("media-playlist");
|
|
pending_stream->uri = g_strdup (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;
|
|
|
|
return playlist;
|
|
}
|
|
|
|
/* Beginning of the actual master playlist parsing */
|
|
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 non-empty line without EXT-STREAM-INF, dropping",
|
|
data);
|
|
goto next_line;
|
|
}
|
|
|
|
uri = uri_join (base_uri, data);
|
|
if (uri == NULL)
|
|
goto next_line;
|
|
|
|
pending_stream->name = name =
|
|
generate_variant_stream_name (uri, pending_stream->bandwidth);
|
|
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 if ((existing_stream =
|
|
find_variant_stream_for_fallback (playlist->variants,
|
|
pending_stream))) {
|
|
GST_DEBUG ("Adding to %s fallback URI %s", existing_stream->name,
|
|
pending_stream->uri);
|
|
existing_stream->fallback =
|
|
g_list_append (existing_stream->fallback,
|
|
g_strdup (pending_stream->uri));
|
|
gst_hls_variant_stream_unref (pending_stream);
|
|
} else {
|
|
GST_INFO ("stream %s @ %u: %s", name, pending_stream->bandwidth, uri);
|
|
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 = gst_hls_variant_parse (data, base_uri);
|
|
|
|
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:")) {
|
|
GstHLSRenditionStream *media;
|
|
|
|
media = gst_m3u8_parse_media (data + strlen ("#EXT-X-MEDIA:"), base_uri);
|
|
|
|
if (media == NULL)
|
|
goto next_line;
|
|
|
|
if (g_list_find_custom (playlist->renditions, media,
|
|
(GCompareFunc) hls_media_compare_func)) {
|
|
GST_DEBUG ("Dropping duplicate alternate rendition group : %s", data);
|
|
gst_hls_rendition_stream_unref (media);
|
|
goto next_line;
|
|
}
|
|
playlist->renditions = g_list_append (playlist->renditions, media);
|
|
GST_INFO ("Stored media %s / group %s", media->name, media->group_id);
|
|
} 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);
|
|
|
|
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);
|
|
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
/* Sanity check : If there are no codecs, a stream shouldn't point to
|
|
* alternate rendition groups.
|
|
*
|
|
* Write a warning to help with further debugging if this causes issues
|
|
* later */
|
|
for (tmp = playlist->variants; tmp; tmp = tmp->next) {
|
|
GstHLSVariantStream *stream = tmp->data;
|
|
|
|
if (stream->codecs == NULL) {
|
|
if (stream->media_groups[0] || stream->media_groups[1]
|
|
|| stream->media_groups[2] || stream->media_groups[3]) {
|
|
GST_WARNING
|
|
("Variant specifies alternate rendition groups but has no codecs specified");
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Filter out audio-only variants from audio+video stream */
|
|
for (tmp = playlist->variants; tmp; tmp = tmp->next) {
|
|
GstHLSVariantStream *stream = tmp->data;
|
|
|
|
most_seen_types |= stream->codecs_stream_type;
|
|
}
|
|
|
|
/* Flag the playlist to indicate whether all codecs are known or not on variants */
|
|
playlist->have_codecs = most_seen_types != 0;
|
|
|
|
GST_DEBUG ("have_codecs:%d most_seen_types:%d", playlist->have_codecs,
|
|
most_seen_types);
|
|
|
|
/* Filter out audio-only variants from audio+video stream */
|
|
if (playlist->have_codecs && most_seen_types != GST_STREAM_TYPE_AUDIO) {
|
|
tmp = playlist->variants;
|
|
while (tmp) {
|
|
GstHLSVariantStream *stream = tmp->data;
|
|
|
|
if (stream->codecs_stream_type != most_seen_types &&
|
|
stream->codecs_stream_type == GST_STREAM_TYPE_AUDIO) {
|
|
GST_DEBUG ("Remove variant with partial stream types %s", stream->name);
|
|
tmp = playlist->variants = g_list_remove (playlist->variants, stream);
|
|
gst_hls_variant_stream_unref (stream);
|
|
} else
|
|
tmp = tmp->next;
|
|
}
|
|
}
|
|
|
|
if (playlist->renditions) {
|
|
guint i;
|
|
/* Assign information from variants to alternate rendition groups. Note that
|
|
* at this point we know that there are caps present on the variants */
|
|
for (tmp = playlist->variants; tmp; tmp = tmp->next) {
|
|
GstHLSVariantStream *stream = tmp->data;
|
|
|
|
GST_DEBUG ("Post-processing Variant Stream '%s'", stream->name);
|
|
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
gchar *alt_rend_group = stream->media_groups[i];
|
|
|
|
if (alt_rend_group) {
|
|
gboolean alt_in_variant = FALSE;
|
|
GstCaps *media_caps = stream_get_media_caps (stream, i);
|
|
GList *altlist;
|
|
if (!media_caps)
|
|
continue;
|
|
for (altlist = playlist->renditions; altlist; altlist = altlist->next) {
|
|
GstHLSRenditionStream *media = altlist->data;
|
|
if (media->mtype != i
|
|
|| g_strcmp0 (media->group_id, alt_rend_group))
|
|
continue;
|
|
GST_DEBUG (" %s caps:%" GST_PTR_FORMAT " media %s, uri: %s",
|
|
GST_HLS_RENDITION_STREAM_TYPE_NAME (i), media_caps, media->name,
|
|
media->uri);
|
|
if (media->uri == NULL) {
|
|
GST_DEBUG (" Media is present in main variant stream");
|
|
alt_in_variant = TRUE;
|
|
} else {
|
|
/* Assign caps to media */
|
|
if (media->caps && !gst_caps_is_equal (media->caps, media_caps)) {
|
|
GST_ERROR (" Media already has different caps %"
|
|
GST_PTR_FORMAT, media->caps);
|
|
} else {
|
|
GST_DEBUG (" Assigning caps %" GST_PTR_FORMAT, media_caps);
|
|
media->caps = gst_caps_ref (media_caps);
|
|
}
|
|
}
|
|
}
|
|
if (!alt_in_variant) {
|
|
GstCaps *new_caps = gst_caps_subtract (stream->caps, media_caps);
|
|
gst_caps_replace (&stream->caps, new_caps);
|
|
}
|
|
gst_caps_unref (media_caps);
|
|
}
|
|
}
|
|
GST_DEBUG ("Stream Ends up with caps %" GST_PTR_FORMAT, stream->caps);
|
|
}
|
|
}
|
|
|
|
GST_DEBUG
|
|
("parsed master playlist with %d streams, %d I-frame streams and %d alternative rendition groups",
|
|
g_list_length (playlist->variants),
|
|
g_list_length (playlist->iframe_variants),
|
|
g_list_length (playlist->renditions));
|
|
|
|
|
|
return playlist;
|
|
}
|
|
|
|
GstHLSVariantStream *
|
|
hls_master_playlist_get_variant_for_bitrate (GstHLSMasterPlaylist *
|
|
playlist, GstHLSVariantStream * current_variant, guint bitrate,
|
|
guint min_bitrate)
|
|
{
|
|
GstHLSVariantStream *variant = current_variant;
|
|
GstHLSVariantStream *variant_by_min = 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 >= min_bitrate)
|
|
variant_by_min = variant;
|
|
if (variant->bandwidth <= bitrate)
|
|
break;
|
|
l = l->prev;
|
|
}
|
|
|
|
/* If variant bitrate is above the min_bitrate (or min_bitrate == 0)
|
|
* return it now */
|
|
if (variant && variant->bandwidth >= min_bitrate)
|
|
return variant;
|
|
|
|
/* Otherwise, return the last (lowest bitrate) variant we saw that
|
|
* was higher than the min_bitrate */
|
|
return variant_by_min;
|
|
}
|
|
|
|
static gboolean
|
|
remove_uncommon (GQuark field_id, GValue * value, GstStructure * st2)
|
|
{
|
|
const GValue *other;
|
|
GValue dest = G_VALUE_INIT;
|
|
|
|
other = gst_structure_id_get_value (st2, field_id);
|
|
|
|
if (other == NULL || (G_VALUE_TYPE (value) != G_VALUE_TYPE (other)))
|
|
return FALSE;
|
|
|
|
if (!gst_value_intersect (&dest, value, other))
|
|
return FALSE;
|
|
|
|
g_value_reset (value);
|
|
g_value_copy (&dest, value);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Merge all common structures from caps1 and caps2
|
|
*
|
|
* Returns empty caps if a structure is not present in both */
|
|
static GstCaps *
|
|
gst_caps_merge_common (GstCaps * caps1, GstCaps * caps2)
|
|
{
|
|
guint it1, it2;
|
|
GstCaps *res = gst_caps_new_empty ();
|
|
|
|
for (it1 = 0; it1 < gst_caps_get_size (caps1); it1++) {
|
|
GstStructure *st1 = gst_caps_get_structure (caps1, it1);
|
|
GstStructure *merged = NULL;
|
|
const gchar *name1 = gst_structure_get_name (st1);
|
|
|
|
for (it2 = 0; it2 < gst_caps_get_size (caps2); it2++) {
|
|
GstStructure *st2 = gst_caps_get_structure (caps2, it2);
|
|
if (gst_structure_has_name (st2, name1)) {
|
|
if (merged == NULL)
|
|
merged = gst_structure_copy (st1);
|
|
gst_structure_filter_and_map_in_place (merged,
|
|
(GstStructureFilterMapFunc) remove_uncommon, st2);
|
|
}
|
|
}
|
|
|
|
if (merged == NULL)
|
|
goto fail;
|
|
gst_caps_append_structure (res, merged);
|
|
}
|
|
|
|
return res;
|
|
|
|
fail:
|
|
{
|
|
GST_WARNING ("Failed to create common caps of %"
|
|
GST_PTR_FORMAT " and %" GST_PTR_FORMAT, caps1, caps2);
|
|
gst_caps_unref (res);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
GstCaps *
|
|
hls_master_playlist_get_common_caps (GstHLSMasterPlaylist * playlist)
|
|
{
|
|
GList *tmp;
|
|
GstCaps *res = NULL;
|
|
|
|
for (tmp = playlist->variants; tmp; tmp = tmp->next) {
|
|
GstHLSVariantStream *stream = tmp->data;
|
|
|
|
GST_DEBUG ("stream caps %" GST_PTR_FORMAT, stream->caps);
|
|
if (!stream->caps) {
|
|
/* If one of the stream doesn't have *any* caps, we can't reliably return
|
|
* any common caps */
|
|
if (res)
|
|
gst_caps_unref (res);
|
|
res = NULL;
|
|
goto beach;
|
|
}
|
|
if (!res) {
|
|
res = gst_caps_copy (stream->caps);
|
|
} else {
|
|
res = gst_caps_merge_common (res, stream->caps);
|
|
if (!res)
|
|
goto beach;
|
|
}
|
|
}
|
|
|
|
res = gst_caps_simplify (res);
|
|
|
|
beach:
|
|
GST_DEBUG ("Returning common caps %" GST_PTR_FORMAT, res);
|
|
|
|
return res;
|
|
}
|
|
|
|
GstStreamType
|
|
gst_stream_type_from_hls_type (GstHLSRenditionStreamType mtype)
|
|
{
|
|
switch (mtype) {
|
|
case GST_HLS_RENDITION_STREAM_TYPE_AUDIO:
|
|
return GST_STREAM_TYPE_AUDIO;
|
|
case GST_HLS_RENDITION_STREAM_TYPE_VIDEO:
|
|
return GST_STREAM_TYPE_VIDEO;
|
|
case GST_HLS_RENDITION_STREAM_TYPE_SUBTITLES:
|
|
return GST_STREAM_TYPE_TEXT;
|
|
default:
|
|
return GST_STREAM_TYPE_UNKNOWN;
|
|
}
|
|
}
|