gstreamer/subprojects/gst-plugins-good/ext/adaptivedemux2/hls/gsthlsdemux.c
Jan Schmidt 268a25b979 hlsdemux2: Use partial segment for playlist update interval
When playing LL-HLS playlists in LL-HLS mode, update the playlist more often (on
the partial segment interval) or else we end up downloading them in bursts and
playing further from the live edge than intended.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/3883>
2023-02-03 16:52:22 +00:00

1304 lines
40 KiB
C

/* GStreamer
* Copyright (C) 2010 Marc-Andre Lureau <marcandre.lureau@gmail.com>
* Copyright (C) 2010 Andoni Morales Alastruey <ylatuya@gmail.com>
* Copyright (C) 2011, Hewlett-Packard Development Company, L.P.
* Author: Youness Alaoui <youness.alaoui@collabora.co.uk>, Collabora Ltd.
* Author: Sebastian Dröge <sebastian.droege@collabora.co.uk>, Collabora Ltd.
* Copyright (C) 2014 Sebastian Dröge <sebastian@centricular.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>
*
* Gsthlsdemux.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.
*/
/**
* SECTION:element-hlsdemux2
* @title: hlsdemux2
*
* HTTP Live Streaming demuxer element.
*
* ## Example launch line
* |[
* gst-launch-1.0 playbin3 uri=http://devimages.apple.com/iphone/samples/bipbop/gear4/prog_index.m3u8
* ]|
*
* Since: 1.22
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <string.h>
#include <gst/base/gsttypefindhelper.h>
#include <gst/tag/tag.h>
#include "gsthlselements.h"
#include "gstadaptivedemuxelements.h"
#include "gsthlsdemux.h"
#include "gsthlsdemux-stream.h"
static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
GST_PAD_ALWAYS,
GST_STATIC_CAPS ("application/x-hls"));
GST_DEBUG_CATEGORY (gst_hls_demux2_debug);
#define GST_CAT_DEFAULT gst_hls_demux2_debug
enum
{
PROP_0,
PROP_START_BITRATE,
PROP_LLHLS_ENABLED,
};
#define DEFAULT_START_BITRATE 0
#define DEFAULT_LLHLS_ENABLED TRUE
/* Maximum values for mpeg-ts DTS values */
#define MPEG_TS_MAX_PTS (((((guint64)1) << 33) * (guint64)100000) / 9)
/* GObject */
static void gst_hls_demux_finalize (GObject * obj);
/* GstElement */
static GstStateChangeReturn
gst_hls_demux_change_state (GstElement * element, GstStateChange transition);
/* GstHLSDemux */
static GstFlowReturn gst_hls_demux_update_playlist (GstHLSDemux * demux,
gboolean update, GError ** err);
static gboolean gst_hls_demux_is_live (GstAdaptiveDemux * demux);
static GstClockTime gst_hls_demux_get_duration (GstAdaptiveDemux * demux);
static gint64 gst_hls_demux_get_manifest_update_interval (GstAdaptiveDemux *
demux);
static gboolean gst_hls_demux_process_manifest (GstAdaptiveDemux * demux,
GstBuffer * buf);
static GstFlowReturn gst_hls_demux_update_manifest (GstAdaptiveDemux * demux);
static void gst_hls_prune_time_mappings (GstHLSDemux * demux);
static gboolean gst_hls_demux_seek (GstAdaptiveDemux * demux, GstEvent * seek);
static gboolean hlsdemux2_element_init (GstPlugin * plugin);
GST_ELEMENT_REGISTER_DEFINE_CUSTOM (hlsdemux2, hlsdemux2_element_init);
typedef struct _GstHLSDemux2 GstHLSDemux2;
typedef struct _GstHLSDemux2Class GstHLSDemux2Class;
#define gst_hls_demux2_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstHLSDemux2, gst_hls_demux2, GST_TYPE_ADAPTIVE_DEMUX,
hls2_element_init ());
static void gst_hls_demux_reset (GstAdaptiveDemux * demux);
static gboolean gst_hls_demux_get_live_seek_range (GstAdaptiveDemux * demux,
gint64 * start, gint64 * stop);
static void gst_hls_demux_set_current_variant (GstHLSDemux * hlsdemux,
GstHLSVariantStream * variant);
static void
gst_hls_demux_finalize (GObject * obj)
{
GstHLSDemux *demux = GST_HLS_DEMUX (obj);
gst_hls_demux_reset (GST_ADAPTIVE_DEMUX_CAST (demux));
g_mutex_clear (&demux->keys_lock);
if (demux->keys) {
g_hash_table_unref (demux->keys);
demux->keys = NULL;
}
G_OBJECT_CLASS (parent_class)->finalize (obj);
}
static void
gst_hls_demux_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstHLSDemux *demux = GST_HLS_DEMUX (object);
switch (prop_id) {
case PROP_START_BITRATE:
demux->start_bitrate = g_value_get_uint (value);
break;
case PROP_LLHLS_ENABLED:
demux->llhls_enabled = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_hls_demux_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstHLSDemux *demux = GST_HLS_DEMUX (object);
switch (prop_id) {
case PROP_START_BITRATE:
g_value_set_uint (value, demux->start_bitrate);
break;
case PROP_LLHLS_ENABLED:
g_value_set_boolean (value, demux->llhls_enabled);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_hls_demux2_class_init (GstHLSDemux2Class * klass)
{
GObjectClass *gobject_class;
GstElementClass *element_class;
GstAdaptiveDemuxClass *adaptivedemux_class;
gobject_class = (GObjectClass *) klass;
element_class = (GstElementClass *) klass;
adaptivedemux_class = (GstAdaptiveDemuxClass *) klass;
gobject_class->set_property = gst_hls_demux_set_property;
gobject_class->get_property = gst_hls_demux_get_property;
gobject_class->finalize = gst_hls_demux_finalize;
g_object_class_install_property (gobject_class, PROP_START_BITRATE,
g_param_spec_uint ("start-bitrate", "Starting Bitrate",
"Initial bitrate to use to choose first alternate (0 = automatic) (bits/s)",
0, G_MAXUINT, DEFAULT_START_BITRATE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_LLHLS_ENABLED,
g_param_spec_boolean ("llhls-enabled", "Enable LL-HLS support",
"Enable support for LL-HLS (Low Latency HLS) downloads",
DEFAULT_LLHLS_ENABLED, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
element_class->change_state = GST_DEBUG_FUNCPTR (gst_hls_demux_change_state);
gst_element_class_add_static_pad_template (element_class, &sinktemplate);
gst_element_class_set_static_metadata (element_class,
"HLS Demuxer",
"Codec/Demuxer/Adaptive",
"HTTP Live Streaming demuxer",
"Edward Hervey <edward@centricular.com>\n"
"Jan Schmidt <jan@centricular.com>");
adaptivedemux_class->is_live = gst_hls_demux_is_live;
adaptivedemux_class->get_live_seek_range = gst_hls_demux_get_live_seek_range;
adaptivedemux_class->get_duration = gst_hls_demux_get_duration;
adaptivedemux_class->get_manifest_update_interval =
gst_hls_demux_get_manifest_update_interval;
adaptivedemux_class->process_manifest = gst_hls_demux_process_manifest;
adaptivedemux_class->update_manifest = gst_hls_demux_update_manifest;
adaptivedemux_class->reset = gst_hls_demux_reset;
adaptivedemux_class->seek = gst_hls_demux_seek;
}
static void
gst_hls_demux2_init (GstHLSDemux * demux)
{
demux->llhls_enabled = DEFAULT_LLHLS_ENABLED;
demux->keys = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
g_mutex_init (&demux->keys_lock);
}
static GstStateChangeReturn
gst_hls_demux_change_state (GstElement * element, GstStateChange transition)
{
GstStateChangeReturn ret;
GstHLSDemux *demux = GST_HLS_DEMUX (element);
switch (transition) {
case GST_STATE_CHANGE_READY_TO_PAUSED:
gst_hls_demux_reset (GST_ADAPTIVE_DEMUX_CAST (demux));
break;
default:
break;
}
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
switch (transition) {
case GST_STATE_CHANGE_PAUSED_TO_READY:
gst_hls_demux_reset (GST_ADAPTIVE_DEMUX_CAST (demux));
g_hash_table_remove_all (demux->keys);
break;
default:
break;
}
return ret;
}
static guint64
gst_hls_demux_get_bitrate (GstHLSDemux * hlsdemux)
{
GstAdaptiveDemux *demux = GST_ADAPTIVE_DEMUX_CAST (hlsdemux);
/* FIXME !!!
*
* No, there isn't a single output :D */
/* Valid because hlsdemux only has a single output */
if (demux->input_period->streams) {
GstAdaptiveDemux2Stream *stream = demux->input_period->streams->data;
return stream->current_download_rate;
}
return 0;
}
static void
gst_hls_demux_clear_all_pending_data (GstHLSDemux * hlsdemux)
{
GstAdaptiveDemux *demux = (GstAdaptiveDemux *) hlsdemux;
GList *walk;
if (!demux->input_period)
return;
for (walk = demux->input_period->streams; walk != NULL; walk = walk->next) {
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (walk->data);
gst_hls_demux_stream_clear_pending_data (hls_stream, TRUE);
}
}
#define SEEK_UPDATES_PLAY_POSITION(r, start_type, stop_type) \
((r >= 0 && start_type != GST_SEEK_TYPE_NONE) || \
(r < 0 && stop_type != GST_SEEK_TYPE_NONE))
#define IS_SNAP_SEEK(f) (f & (GST_SEEK_FLAG_SNAP_BEFORE | \
GST_SEEK_FLAG_SNAP_AFTER | \
GST_SEEK_FLAG_SNAP_NEAREST | \
GST_SEEK_FLAG_TRICKMODE_KEY_UNITS | \
GST_SEEK_FLAG_KEY_UNIT))
static gboolean
gst_hls_demux_seek (GstAdaptiveDemux * demux, GstEvent * seek)
{
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
GstFormat format;
GstSeekFlags flags;
GstSeekType start_type, stop_type;
gint64 start, stop;
gdouble rate, old_rate;
GList *walk;
gint64 current_pos, target_pos, final_pos;
guint64 bitrate;
gst_event_parse_seek (seek, &rate, &format, &flags, &start_type, &start,
&stop_type, &stop);
if (!SEEK_UPDATES_PLAY_POSITION (rate, start_type, stop_type)) {
/* nothing to do if we don't have to update the current position */
return TRUE;
}
old_rate = demux->segment.rate;
bitrate = gst_hls_demux_get_bitrate (hlsdemux);
/* Use I-frame variants for trick modes */
if (hlsdemux->master->iframe_variants != NULL
&& rate < -1.0 && old_rate >= -1.0 && old_rate <= 1.0) {
GError *err = NULL;
/* Switch to I-frame variant */
gst_hls_demux_set_current_variant (hlsdemux,
hlsdemux->master->iframe_variants->data);
if (gst_hls_demux_update_playlist (hlsdemux, FALSE, &err) != GST_FLOW_OK) {
GST_ELEMENT_ERROR_FROM_ERROR (hlsdemux, "Could not switch playlist", err);
return FALSE;
}
//hlsdemux->discont = TRUE;
gst_hls_demux_change_variant_playlist (hlsdemux, bitrate / ABS (rate),
NULL);
} else if (rate > -1.0 && rate <= 1.0 && (old_rate < -1.0 || old_rate > 1.0)) {
GError *err = NULL;
/* Switch to normal variant */
gst_hls_demux_set_current_variant (hlsdemux,
hlsdemux->master->variants->data);
if (gst_hls_demux_update_playlist (hlsdemux, FALSE, &err) != GST_FLOW_OK) {
GST_ELEMENT_ERROR_FROM_ERROR (hlsdemux, "Could not switch playlist", err);
return FALSE;
}
//hlsdemux->discont = TRUE;
/* TODO why not continue using the same? that was being used up to now? */
gst_hls_demux_change_variant_playlist (hlsdemux, bitrate, NULL);
}
target_pos = rate < 0 ? stop : start;
final_pos = target_pos;
/* properly cleanup pending decryption status */
if (flags & GST_SEEK_FLAG_FLUSH) {
gst_hls_demux_clear_all_pending_data (hlsdemux);
gst_hls_prune_time_mappings (hlsdemux);
}
for (walk = demux->input_period->streams; walk; walk = g_list_next (walk)) {
GstAdaptiveDemux2Stream *stream =
GST_ADAPTIVE_DEMUX2_STREAM_CAST (walk->data);
/* Only seek on selected streams */
if (!gst_adaptive_demux2_stream_is_selected (stream))
continue;
if (gst_hls_demux_stream_seek (stream, rate >= 0, flags, target_pos,
&current_pos) != GST_FLOW_OK) {
GST_ERROR_OBJECT (stream, "Failed to seek on stream");
return FALSE;
}
/* FIXME: use minimum position always ? */
if (final_pos > current_pos)
final_pos = current_pos;
}
if (IS_SNAP_SEEK (flags)) {
if (rate >= 0)
gst_segment_do_seek (&demux->segment, rate, format, flags, start_type,
final_pos, stop_type, stop, NULL);
else
gst_segment_do_seek (&demux->segment, rate, format, flags, start_type,
start, stop_type, final_pos, NULL);
}
return TRUE;
}
static GstFlowReturn
gst_hls_demux_update_manifest (GstAdaptiveDemux * demux)
{
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
return gst_hls_demux_update_playlist (hlsdemux, TRUE, NULL);
}
static GstAdaptiveDemux2Stream *
create_common_hls_stream (GstHLSDemux * demux, const gchar * name)
{
GstAdaptiveDemux2Stream *stream;
stream = g_object_new (GST_TYPE_HLS_DEMUX_STREAM, "name", name, NULL);
GST_HLS_DEMUX_STREAM (stream)->llhls_enabled = demux->llhls_enabled;
gst_adaptive_demux2_add_stream ((GstAdaptiveDemux *) demux, stream);
return stream;
}
static void
create_main_variant_stream (GstHLSDemux * demux)
{
GstAdaptiveDemux2Stream *stream;
GstHLSDemuxStream *hlsdemux_stream;
GST_DEBUG_OBJECT (demux, "Creating main variant stream");
stream = create_common_hls_stream (demux, "hlsstream-variant");
demux->main_stream = hlsdemux_stream = (GstHLSDemuxStream *) stream;
hlsdemux_stream->is_variant = TRUE;
hlsdemux_stream->playlist_fetched = TRUE;
/* Due to HLS manifest information being so unreliable/inconsistent, we will
* create the actual tracks once we have information about the streams present
* in the variant data stream */
stream->pending_tracks = TRUE;
}
GstAdaptiveDemuxTrack *
gst_hls_demux_new_track_for_rendition (GstHLSDemux * demux,
GstHLSRenditionStream * rendition,
GstCaps * caps, GstStreamFlags flags, GstTagList * tags)
{
GstAdaptiveDemuxTrack *track;
gchar *stream_id;
GstStreamType stream_type = gst_stream_type_from_hls_type (rendition->mtype);
if (rendition->name)
stream_id =
g_strdup_printf ("%s-%s", gst_stream_type_get_name (stream_type),
rendition->name);
else if (rendition->lang)
stream_id =
g_strdup_printf ("%s-%s", gst_stream_type_get_name (stream_type),
rendition->lang);
else
stream_id = g_strdup (gst_stream_type_get_name (stream_type));
if (rendition->lang) {
if (tags == NULL)
tags = gst_tag_list_new_empty ();
if (gst_tag_check_language_code (rendition->lang))
gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_LANGUAGE_CODE,
rendition->lang, NULL);
else
gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, GST_TAG_LANGUAGE_NAME,
rendition->lang, NULL);
}
if (stream_type == GST_STREAM_TYPE_TEXT)
flags |= GST_STREAM_FLAG_SPARSE;
if (rendition->is_default)
flags |= GST_STREAM_FLAG_SELECT;
track =
gst_adaptive_demux_track_new ((GstAdaptiveDemux *) demux, stream_type,
flags, stream_id, caps, tags);
g_free (stream_id);
return track;
}
static GstHLSDemuxStream *
create_rendition_stream (GstHLSDemux * demux, GstHLSRenditionStream * media)
{
GstAdaptiveDemux2Stream *stream;
GstAdaptiveDemuxTrack *track;
GstHLSDemuxStream *hlsdemux_stream;
gchar *stream_name;
GST_DEBUG_OBJECT (demux,
"Creating stream for media %s lang:%s (%" GST_PTR_FORMAT ")", media->name,
media->lang, media->caps);
/* We can't reliably provide caps for HLS target tracks since they might
* change at any point in time */
track = gst_hls_demux_new_track_for_rendition (demux, media, NULL, 0, NULL);
stream_name = g_strdup_printf ("hlsstream-%s", track->stream_id);
stream = create_common_hls_stream (demux, stream_name);
g_free (stream_name);
hlsdemux_stream = (GstHLSDemuxStream *) stream;
hlsdemux_stream->is_variant = FALSE;
hlsdemux_stream->playlist_fetched = FALSE;
stream->stream_type = hlsdemux_stream->rendition_type =
gst_stream_type_from_hls_type (media->mtype);
if (media->lang)
hlsdemux_stream->lang = g_strdup (media->lang);
if (media->name)
hlsdemux_stream->name = g_strdup (media->name);
gst_adaptive_demux2_stream_add_track (stream, track);
gst_adaptive_demux_track_unref (track);
return hlsdemux_stream;
}
static GstHLSDemuxStream *
existing_rendition_stream (GList * streams, GstHLSRenditionStream * media)
{
GList *tmp;
GstStreamType stream_type = gst_stream_type_from_hls_type (media->mtype);
for (tmp = streams; tmp; tmp = tmp->next) {
GstHLSDemuxStream *demux_stream = tmp->data;
if (demux_stream->is_variant)
continue;
if (demux_stream->rendition_type == stream_type) {
if (!g_strcmp0 (demux_stream->name, media->name))
return demux_stream;
if (media->lang && !g_strcmp0 (demux_stream->lang, media->lang))
return demux_stream;
}
}
return NULL;
}
static gboolean
gst_hls_demux_setup_streams (GstAdaptiveDemux * demux)
{
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
GstHLSVariantStream *playlist = hlsdemux->current_variant;
GList *tmp;
GList *streams = NULL;
if (playlist == NULL) {
GST_WARNING_OBJECT (demux, "Can't configure streams - no variant selected");
return FALSE;
}
GST_DEBUG_OBJECT (demux, "Setting up streams");
/* If there are alternate renditions, we will produce a GstAdaptiveDemux2Stream
* and GstAdaptiveDemuxTrack for each combination of GstStreamType and other
* unique identifier (for now just language)
*
* Which actual GstHLSMedia to use for each stream will be determined based on
* the `group-id` (if present and more than one) selected on the main variant
* stream */
for (tmp = hlsdemux->master->renditions; tmp; tmp = tmp->next) {
GstHLSRenditionStream *media = tmp->data;
GstHLSDemuxStream *media_stream, *previous_media_stream;
GST_LOG_OBJECT (demux, "Rendition %s name:'%s' lang:'%s' uri:%s",
gst_stream_type_get_name (gst_stream_type_from_hls_type (media->mtype)),
media->name, media->lang, media->uri);
if (media->uri == NULL) {
GST_DEBUG_OBJECT (demux,
"Skipping media '%s' , it's provided by the variant stream",
media->name);
continue;
}
media_stream = previous_media_stream =
existing_rendition_stream (streams, media);
if (!media_stream) {
media_stream = create_rendition_stream (hlsdemux, tmp->data);
} else
GST_DEBUG_OBJECT (demux, "Re-using existing GstHLSDemuxStream %s %s",
media_stream->name, media_stream->lang);
/* Is this rendition active in the current variant ? */
if (!g_strcmp0 (playlist->media_groups[media->mtype], media->group_id)) {
GST_DEBUG_OBJECT (demux, "Enabling rendition");
if (media_stream->current_rendition)
gst_hls_rendition_stream_unref (media_stream->current_rendition);
media_stream->current_rendition = gst_hls_rendition_stream_ref (media);
}
if (!previous_media_stream)
streams = g_list_append (streams, media_stream);
}
/* Free the list (but not the contents, which are stored
* elsewhere */
if (streams)
g_list_free (streams);
create_main_variant_stream (hlsdemux);
return TRUE;
}
static void
gst_hls_demux_set_current_variant (GstHLSDemux * hlsdemux,
GstHLSVariantStream * variant)
{
if (hlsdemux->current_variant == variant || variant == NULL)
return;
if (hlsdemux->current_variant != NULL) {
GST_DEBUG_OBJECT (hlsdemux, "Will switch from variant '%s' to '%s'",
hlsdemux->current_variant->name, variant->name);
if (hlsdemux->pending_variant) {
GST_ERROR_OBJECT (hlsdemux, "Already waiting for pending variant '%s'",
hlsdemux->pending_variant->name);
gst_hls_variant_stream_unref (hlsdemux->pending_variant);
}
hlsdemux->pending_variant = gst_hls_variant_stream_ref (variant);
} else {
GST_DEBUG_OBJECT (hlsdemux, "Setting variant '%s'", variant->name);
hlsdemux->current_variant = gst_hls_variant_stream_ref (variant);
}
}
static gboolean
gst_hls_demux_process_manifest (GstAdaptiveDemux * demux, GstBuffer * buf)
{
GstHLSVariantStream *variant;
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
gchar *playlist = NULL;
gboolean ret;
GstHLSMediaPlaylist *simple_media_playlist = NULL;
GST_INFO_OBJECT (demux, "Initial playlist location: %s (base uri: %s)",
demux->manifest_uri, demux->manifest_base_uri);
playlist = gst_hls_buf_to_utf8_text (buf);
if (playlist == NULL) {
GST_WARNING_OBJECT (demux, "Error validating initial playlist");
return FALSE;
}
if (hlsdemux->master) {
gst_hls_master_playlist_unref (hlsdemux->master);
hlsdemux->master = NULL;
}
hlsdemux->master = gst_hls_master_playlist_new_from_data (playlist,
gst_adaptive_demux_get_manifest_ref_uri (demux));
if (hlsdemux->master == NULL) {
/* In most cases, this will happen if we set a wrong url in the
* source element and we have received the 404 HTML response instead of
* the playlist */
GST_ELEMENT_ERROR (demux, STREAM, DECODE, ("Invalid playlist."),
("Could not parse playlist. Check if the URL is correct."));
return FALSE;
}
if (hlsdemux->master->is_simple) {
simple_media_playlist =
gst_hls_media_playlist_parse (playlist, GST_CLOCK_TIME_NONE,
gst_adaptive_demux_get_manifest_ref_uri (demux), NULL);
}
/* select the initial variant stream */
if (demux->connection_speed == 0) {
variant = hlsdemux->master->default_variant;
} else if (hlsdemux->start_bitrate > 0) {
variant =
gst_hls_master_playlist_get_variant_for_bitrate (hlsdemux->master,
NULL, hlsdemux->start_bitrate, demux->min_bitrate);
} else {
variant =
gst_hls_master_playlist_get_variant_for_bitrate (hlsdemux->master,
NULL, demux->connection_speed, demux->min_bitrate);
}
if (variant) {
GST_INFO_OBJECT (hlsdemux,
"Manifest processed, initial variant selected : `%s`", variant->name);
gst_hls_demux_set_current_variant (hlsdemux, variant); // FIXME: inline?
}
GST_DEBUG_OBJECT (hlsdemux, "Manifest handled, now setting up streams");
ret = gst_hls_demux_setup_streams (demux);
if (simple_media_playlist) {
GstM3U8SeekResult seek_result;
hlsdemux->main_stream->playlist = simple_media_playlist;
if (!gst_hls_media_playlist_get_starting_segment (simple_media_playlist,
hlsdemux->main_stream->llhls_enabled, &seek_result)) {
GST_DEBUG_OBJECT (hlsdemux->main_stream,
"Failed to find a segment to start at");
return FALSE;
}
hlsdemux->main_stream->current_segment = seek_result.segment;
hlsdemux->main_stream->in_partial_segments =
seek_result.found_partial_segment;
hlsdemux->main_stream->part_idx = seek_result.part_idx;
gst_hls_demux_setup_initial_playlist (hlsdemux, simple_media_playlist);
gst_hls_update_time_mappings (hlsdemux, simple_media_playlist);
gst_hls_media_playlist_dump (simple_media_playlist);
}
/* get the selected media playlist (unless the initial list was one already) */
if (!hlsdemux->master->is_simple) {
GError *err = NULL;
if (gst_hls_demux_update_playlist (hlsdemux, FALSE, &err) != GST_FLOW_OK) {
GST_ELEMENT_ERROR_FROM_ERROR (demux, "Could not fetch media playlist",
err);
return FALSE;
}
}
return ret;
}
static GstClockTime
gst_hls_demux_get_duration (GstAdaptiveDemux * demux)
{
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
GstClockTime duration = GST_CLOCK_TIME_NONE;
if (hlsdemux->main_stream)
duration =
gst_hls_media_playlist_get_duration (hlsdemux->main_stream->playlist);
return duration;
}
static gboolean
gst_hls_demux_is_live (GstAdaptiveDemux * demux)
{
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
gboolean is_live = FALSE;
if (hlsdemux->main_stream && hlsdemux->main_stream->playlist)
is_live = gst_hls_media_playlist_is_live (hlsdemux->main_stream->playlist);
return is_live;
}
const GstHLSKey *
gst_hls_demux_get_key (GstHLSDemux * demux, const gchar * key_url,
const gchar * referer, gboolean allow_cache)
{
GstAdaptiveDemux *adaptive_demux = GST_ADAPTIVE_DEMUX (demux);
DownloadRequest *key_request;
DownloadFlags dl_flags = DOWNLOAD_FLAG_NONE;
GstBuffer *key_buffer;
GstHLSKey *key;
GError *err = NULL;
GST_LOG_OBJECT (demux, "Looking up key for key url %s", key_url);
g_mutex_lock (&demux->keys_lock);
key = g_hash_table_lookup (demux->keys, key_url);
if (key != NULL) {
GST_LOG_OBJECT (demux, "Found key for key url %s in key cache", key_url);
goto out;
}
GST_INFO_OBJECT (demux, "Fetching key %s", key_url);
if (!allow_cache)
dl_flags |= DOWNLOAD_FLAG_FORCE_REFRESH;
key_request =
downloadhelper_fetch_uri (adaptive_demux->download_helper,
key_url, referer, dl_flags, &err);
if (key_request == NULL) {
GST_WARNING_OBJECT (demux, "Failed to download key to decrypt data: %s",
err ? err->message : "error");
g_clear_error (&err);
goto out;
}
key_buffer = download_request_take_buffer (key_request);
download_request_unref (key_request);
key = g_new0 (GstHLSKey, 1);
if (gst_buffer_extract (key_buffer, 0, key->data, 16) < 16)
GST_WARNING_OBJECT (demux, "Download decryption key is too short!");
g_hash_table_insert (demux->keys, g_strdup (key_url), key);
gst_buffer_unref (key_buffer);
out:
g_mutex_unlock (&demux->keys_lock);
if (key != NULL)
GST_MEMDUMP_OBJECT (demux, "Key", key->data, 16);
return key;
}
void
gst_hls_demux_start_rendition_streams (GstHLSDemux * hlsdemux)
{
GstAdaptiveDemux *demux = (GstAdaptiveDemux *) hlsdemux;
GList *tmp;
for (tmp = demux->input_period->streams; tmp; tmp = tmp->next) {
GstAdaptiveDemux2Stream *stream = (GstAdaptiveDemux2Stream *) tmp->data;
GstHLSDemuxStream *hls_stream = (GstHLSDemuxStream *) stream;
if (!hls_stream->is_variant
&& gst_adaptive_demux2_stream_is_selected (stream))
gst_adaptive_demux2_stream_start (stream);
}
}
static GstHLSTimeMap *
time_map_in_list (GList * list, gint64 dsn)
{
GList *iter;
for (iter = list; iter; iter = iter->next) {
GstHLSTimeMap *map = iter->data;
if (map->dsn == dsn)
return map;
}
return NULL;
}
GstHLSTimeMap *
gst_hls_demux_find_time_map (GstHLSDemux * demux, gint64 dsn)
{
return time_map_in_list (demux->mappings, dsn);
}
static GstHLSTimeMap *
gst_hls_time_map_new (void)
{
GstHLSTimeMap *map = g_new0 (GstHLSTimeMap, 1);
map->stream_time = GST_CLOCK_TIME_NONE;
map->internal_time = GST_CLOCK_TIME_NONE;
return map;
}
static void
gst_hls_time_map_free (GstHLSTimeMap * map)
{
if (map->pdt)
g_date_time_unref (map->pdt);
g_free (map);
}
void
gst_hls_demux_add_time_mapping (GstHLSDemux * demux, gint64 dsn,
GstClockTimeDiff stream_time, GDateTime * pdt)
{
#ifndef GST_DISABLE_GST_DEBUG
gchar *datestring = NULL;
#endif
GstHLSTimeMap *map;
GList *tmp;
GstClockTime offset = 0;
/* Check if we don't already have a mapping for the given dsn */
for (tmp = demux->mappings; tmp; tmp = tmp->next) {
GstHLSTimeMap *map = tmp->data;
if (map->dsn == dsn) {
#ifndef GST_DISABLE_GST_DEBUG
if (map->pdt)
datestring = g_date_time_format_iso8601 (map->pdt);
GST_DEBUG_OBJECT (demux,
"Already have mapping, dsn:%" G_GINT64_FORMAT " stream_time:%"
GST_TIME_FORMAT " internal_time:%" GST_TIME_FORMAT " pdt:%s",
map->dsn, GST_TIME_ARGS (map->stream_time),
GST_TIME_ARGS (map->internal_time), datestring);
g_free (datestring);
#endif
return;
}
}
#ifndef GST_DISABLE_GST_DEBUG
if (pdt)
datestring = g_date_time_format_iso8601 (pdt);
GST_DEBUG_OBJECT (demux,
"New mapping, dsn:%" G_GINT64_FORMAT " stream_time:%" GST_TIME_FORMAT
" pdt:%s", dsn, GST_TIME_ARGS (stream_time), datestring);
g_free (datestring);
#endif
if (stream_time < 0) {
offset = -stream_time;
stream_time = 0;
/* Handle negative stream times. This can happen for example when the server
* returns an older playlist.
*
* Shift the values accordingly to end up with non-negative reference stream
* time */
GST_DEBUG_OBJECT (demux,
"Shifting values before storage (offset : %" GST_TIME_FORMAT ")",
GST_TIME_ARGS (offset));
}
map = gst_hls_time_map_new ();
map->dsn = dsn;
map->stream_time = stream_time;
if (pdt) {
if (offset)
map->pdt = g_date_time_add (pdt, offset / GST_USECOND);
else
map->pdt = g_date_time_ref (pdt);
}
demux->mappings = g_list_append (demux->mappings, map);
}
/* Remove any time mapping which isn't currently used by any stream playlist */
static void
gst_hls_prune_time_mappings (GstHLSDemux * hlsdemux)
{
GstAdaptiveDemux *demux = (GstAdaptiveDemux *) hlsdemux;
GList *active = NULL;
GList *iterstream;
for (iterstream = demux->input_period->streams; iterstream;
iterstream = iterstream->next) {
GstAdaptiveDemux2Stream *stream = iterstream->data;
GstHLSDemuxStream *hls_stream = (GstHLSDemuxStream *) stream;
gint64 dsn = G_MAXINT64;
guint idx, len;
if (!hls_stream->playlist)
continue;
len = hls_stream->playlist->segments->len;
for (idx = 0; idx < len; idx++) {
GstM3U8MediaSegment *segment =
g_ptr_array_index (hls_stream->playlist->segments, idx);
if (dsn == G_MAXINT64 || segment->discont_sequence != dsn) {
dsn = segment->discont_sequence;
if (!time_map_in_list (active, dsn)) {
GstHLSTimeMap *map = gst_hls_demux_find_time_map (hlsdemux, dsn);
if (map) {
GST_DEBUG_OBJECT (demux,
"Keeping active time map dsn:%" G_GINT64_FORMAT, map->dsn);
/* Move active dsn to active list */
hlsdemux->mappings = g_list_remove (hlsdemux->mappings, map);
active = g_list_append (active, map);
}
}
}
}
}
g_list_free_full (hlsdemux->mappings, (GDestroyNotify) gst_hls_time_map_free);
hlsdemux->mappings = active;
}
/* Go over the DSN from the playlist and add any missing time mapping */
void
gst_hls_update_time_mappings (GstHLSDemux * demux,
GstHLSMediaPlaylist * playlist)
{
guint idx, len = playlist->segments->len;
gint64 dsn = G_MAXINT64;
for (idx = 0; idx < len; idx++) {
GstM3U8MediaSegment *segment = g_ptr_array_index (playlist->segments, idx);
if (dsn == G_MAXINT64 || segment->discont_sequence != dsn) {
dsn = segment->discont_sequence;
if (!gst_hls_demux_find_time_map (demux, segment->discont_sequence))
gst_hls_demux_add_time_mapping (demux, segment->discont_sequence,
segment->stream_time, segment->datetime);
}
}
}
void
gst_hls_demux_setup_initial_playlist (GstHLSDemux * demux,
GstHLSMediaPlaylist * playlist)
{
GstM3U8MediaSegment *segment;
GST_DEBUG_OBJECT (demux,
"Setting up initial variant segment and time mapping");
/* This is the initial variant playlist. We will use it to base all our timing
* from. */
segment = g_ptr_array_index (playlist->segments, 0);
if (segment) {
segment->stream_time = 0;
gst_hls_media_playlist_recalculate_stream_time (playlist, segment);
}
}
/* Reset hlsdemux in case of live synchronization loss (i.e. when a media
* playlist update doesn't match at all with the previous one) */
void
gst_hls_demux_reset_for_lost_sync (GstHLSDemux * hlsdemux)
{
GstAdaptiveDemux *demux = (GstAdaptiveDemux *) hlsdemux;
GList *iter;
GST_DEBUG_OBJECT (hlsdemux, "Resetting for lost sync");
for (iter = demux->input_period->streams; iter; iter = iter->next) {
GstHLSDemuxStream *hls_stream = iter->data;
GstAdaptiveDemux2Stream *stream = (GstAdaptiveDemux2Stream *) hls_stream;
if (hls_stream->current_segment)
gst_m3u8_media_segment_unref (hls_stream->current_segment);
hls_stream->current_segment = NULL;
if (hls_stream->is_variant) {
GstHLSTimeMap *map;
GstM3U8SeekResult seek_result;
/* Resynchronize the variant stream */
g_assert (stream->current_position != GST_CLOCK_STIME_NONE);
if (gst_hls_media_playlist_get_starting_segment (hls_stream->playlist,
hls_stream->llhls_enabled, &seek_result)) {
hls_stream->current_segment = seek_result.segment;
hls_stream->in_partial_segments = seek_result.found_partial_segment;
hls_stream->part_idx = seek_result.part_idx;
hls_stream->current_segment->stream_time = stream->current_position;
gst_hls_media_playlist_recalculate_stream_time (hls_stream->playlist,
hls_stream->current_segment);
GST_DEBUG_OBJECT (stream,
"Resynced variant playlist to %" GST_STIME_FORMAT,
GST_STIME_ARGS (stream->current_position));
map =
gst_hls_demux_find_time_map (hlsdemux,
hls_stream->current_segment->discont_sequence);
if (map)
map->internal_time = GST_CLOCK_TIME_NONE;
gst_hls_update_time_mappings (hlsdemux, hls_stream->playlist);
gst_hls_media_playlist_dump (hls_stream->playlist);
} else {
GST_ERROR_OBJECT (stream, "Failed to locate a segment to restart at!");
}
} else {
/* Force playlist update for the rendition streams, it will resync to the
* variant stream on the next round */
if (hls_stream->playlist)
gst_hls_media_playlist_unref (hls_stream->playlist);
hls_stream->playlist = NULL;
hls_stream->playlist_fetched = FALSE;
}
}
}
static void
gst_hls_demux_reset (GstAdaptiveDemux * ademux)
{
GstHLSDemux *demux = GST_HLS_DEMUX_CAST (ademux);
GST_DEBUG_OBJECT (demux, "resetting");
if (ademux->input_period) {
GList *walk;
for (walk = ademux->input_period->streams; walk != NULL; walk = walk->next) {
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (walk->data);
hls_stream->pdt_tag_sent = FALSE;
}
}
if (demux->master) {
gst_hls_master_playlist_unref (demux->master);
demux->master = NULL;
}
if (demux->current_variant != NULL) {
gst_hls_variant_stream_unref (demux->current_variant);
demux->current_variant = NULL;
}
if (demux->pending_variant != NULL) {
gst_hls_variant_stream_unref (demux->pending_variant);
demux->pending_variant = NULL;
}
g_list_free_full (demux->mappings, (GDestroyNotify) gst_hls_time_map_free);
demux->mappings = NULL;
gst_hls_demux_clear_all_pending_data (demux);
}
static GstFlowReturn
gst_hls_demux_update_variant_playlist (GstHLSDemux * demux, GError ** err)
{
GstFlowReturn ret = GST_FLOW_OK;
GstHLSVariantStream *target_variant =
demux->pending_variant ? demux->pending_variant : demux->current_variant;
GstHLSDemuxStream *stream = demux->main_stream;
ret = gst_hls_demux_stream_update_media_playlist (stream,
&target_variant->uri, err);
if (ret != GST_FLOW_OK)
return ret;
if (demux->pending_variant) {
gst_hls_variant_stream_unref (demux->current_variant);
/* Stealing ref */
demux->current_variant = demux->pending_variant;
demux->pending_variant = NULL;
}
stream->playlist_fetched = TRUE;
return ret;
}
/*
* update: TRUE only when requested from parent class (via
* ::demux_update_manifest() or ::change_variant_playlist() ).
*/
static GstFlowReturn
gst_hls_demux_update_playlist (GstHLSDemux * demux, gboolean update,
GError ** err)
{
GstFlowReturn ret = GST_FLOW_OK;
GstAdaptiveDemux *adaptive_demux = GST_ADAPTIVE_DEMUX (demux);
GST_DEBUG_OBJECT (demux, "update:%d", update);
/* Download and update the appropriate variant playlist (pending if any, else
* current) */
ret = gst_hls_demux_update_variant_playlist (demux, err);
if (ret != GST_FLOW_OK)
return ret;
if (update && gst_hls_demux_is_live (adaptive_demux)) {
GList *tmp;
GST_DEBUG_OBJECT (demux,
"LIVE, Marking rendition streams to be updated next");
/* We're live, instruct all rendition medias to be updated next */
for (tmp = adaptive_demux->input_period->streams; tmp; tmp = tmp->next) {
GstHLSDemuxStream *hls_stream = tmp->data;
if (!hls_stream->is_variant)
hls_stream->playlist_fetched = FALSE;
}
}
return GST_FLOW_OK;
}
gboolean
gst_hls_demux_change_variant_playlist (GstHLSDemux * demux, guint max_bitrate,
gboolean * changed)
{
GstHLSVariantStream *lowest_variant, *lowest_ivariant;
GstHLSVariantStream *previous_variant, *new_variant;
gint old_bandwidth, new_bandwidth;
GstAdaptiveDemux *adaptive_demux = GST_ADAPTIVE_DEMUX_CAST (demux);
GstAdaptiveDemux2Stream *stream;
g_return_val_if_fail (demux->main_stream != NULL, FALSE);
stream = (GstAdaptiveDemux2Stream *) demux->main_stream;
/* Make sure we keep a reference in case we need to switch back */
previous_variant = gst_hls_variant_stream_ref (demux->current_variant);
new_variant =
gst_hls_master_playlist_get_variant_for_bitrate (demux->master,
demux->current_variant, max_bitrate, adaptive_demux->min_bitrate);
retry_failover_protection:
old_bandwidth = previous_variant->bandwidth;
new_bandwidth = new_variant->bandwidth;
/* Don't do anything else if the playlist is the same */
if (new_bandwidth == old_bandwidth) {
gst_hls_variant_stream_unref (previous_variant);
return TRUE;
}
gst_hls_demux_set_current_variant (demux, new_variant);
GST_INFO_OBJECT (demux, "Client was on %dbps, max allowed is %dbps, switching"
" to bitrate %dbps", old_bandwidth, max_bitrate, new_bandwidth);
if (gst_hls_demux_update_playlist (demux, TRUE, NULL) == GST_FLOW_OK) {
const gchar *main_uri;
gchar *uri = new_variant->uri;
main_uri = gst_adaptive_demux_get_manifest_ref_uri (adaptive_demux);
gst_element_post_message (GST_ELEMENT_CAST (demux),
gst_message_new_element (GST_OBJECT_CAST (demux),
gst_structure_new (GST_ADAPTIVE_DEMUX_STATISTICS_MESSAGE_NAME,
"manifest-uri", G_TYPE_STRING,
main_uri, "uri", G_TYPE_STRING,
uri, "bitrate", G_TYPE_INT, new_bandwidth, NULL)));
if (changed)
*changed = TRUE;
stream->discont = TRUE;
} else if (gst_adaptive_demux2_is_running (GST_ADAPTIVE_DEMUX_CAST (demux))) {
GstHLSVariantStream *failover_variant = NULL;
GList *failover;
GST_INFO_OBJECT (demux, "Unable to update playlist. Switching back");
/* we find variants by bitrate by going from highest to lowest, so it's
* possible that there's another variant with the same bitrate before the
* one selected which we can use as failover */
failover = g_list_find (demux->master->variants, new_variant);
if (failover != NULL)
failover = failover->prev;
if (failover != NULL)
failover_variant = failover->data;
if (failover_variant && new_bandwidth == failover_variant->bandwidth) {
new_variant = failover_variant;
goto retry_failover_protection;
}
gst_hls_demux_set_current_variant (demux, previous_variant);
/* Try a lower bitrate (or stop if we just tried the lowest) */
if (previous_variant->iframe) {
lowest_ivariant = demux->master->iframe_variants->data;
if (new_bandwidth == lowest_ivariant->bandwidth) {
gst_hls_variant_stream_unref (previous_variant);
return FALSE;
}
} else {
lowest_variant = demux->master->variants->data;
if (new_bandwidth == lowest_variant->bandwidth) {
gst_hls_variant_stream_unref (previous_variant);
return FALSE;
}
}
gst_hls_variant_stream_unref (previous_variant);
return gst_hls_demux_change_variant_playlist (demux, new_bandwidth - 1,
changed);
}
gst_hls_variant_stream_unref (previous_variant);
return TRUE;
}
static gint64
gst_hls_demux_get_manifest_update_interval (GstAdaptiveDemux * demux)
{
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
GstClockTime target_duration = 5 * GST_SECOND;
GstHLSDemuxStream *main_stream = hlsdemux->main_stream;
if (main_stream) {
target_duration =
gst_hls_demux_stream_get_playlist_reload_interval (main_stream);
}
GST_DEBUG_OBJECT (demux, "Returning update interval of %" GST_TIME_FORMAT,
GST_TIME_ARGS (target_duration));
return gst_util_uint64_scale (target_duration, G_USEC_PER_SEC, GST_SECOND);
}
static gboolean
gst_hls_demux_get_live_seek_range (GstAdaptiveDemux * demux, gint64 * start,
gint64 * stop)
{
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
gboolean ret = FALSE;
if (hlsdemux->main_stream && hlsdemux->main_stream->playlist)
ret =
gst_hls_media_playlist_get_seek_range (hlsdemux->main_stream->playlist,
start, stop);
return ret;
}
static gboolean
hlsdemux2_element_init (GstPlugin * plugin)
{
gboolean ret = TRUE;
GST_DEBUG_CATEGORY_INIT (gst_hls_demux2_debug, "hlsdemux2", 0,
"hlsdemux2 element");
if (!adaptivedemux2_base_element_init (plugin))
return TRUE;
ret = gst_element_register (plugin, "hlsdemux2",
GST_RANK_PRIMARY + 1, GST_TYPE_HLS_DEMUX2);
return ret;
}