mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-09-08 21:28:45 +00:00
Found running `gst-inspect-1.0 -a |& grep -v ":" | grep @` Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4682>
1379 lines
43 KiB
C
1379 lines
43 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 <glib/gi18n-lib.h>
|
|
|
|
/* FIXME: Only needed for scheduler-unlock/lock hack */
|
|
#include <gstadaptivedemux-private.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,
|
|
};
|
|
|
|
#define DEFAULT_START_BITRATE 0
|
|
|
|
/* 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_check_variant_playlist_loaded (GstHLSDemux * demux);
|
|
|
|
static gboolean gst_hls_demux_is_live (GstAdaptiveDemux * demux);
|
|
static GstClockTime gst_hls_demux_get_duration (GstAdaptiveDemux * demux);
|
|
static gboolean gst_hls_demux_process_initial_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;
|
|
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;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
hlsdemux_requires_periodical_playlist_update_default (GstAdaptiveDemux *
|
|
demux G_GNUC_UNUSED)
|
|
{
|
|
/* We don't need the base class to update our manifest periodically, the
|
|
* playlist loader for the main stream will do that and trigger
|
|
* an update manual */
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
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));
|
|
|
|
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>, "
|
|
"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->requires_periodical_playlist_update =
|
|
hlsdemux_requires_periodical_playlist_update_default;
|
|
adaptivedemux_class->process_manifest =
|
|
gst_hls_demux_process_initial_manifest;
|
|
adaptivedemux_class->reset = gst_hls_demux_reset;
|
|
adaptivedemux_class->update_manifest = gst_hls_demux_update_manifest;
|
|
adaptivedemux_class->seek = gst_hls_demux_seek;
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux2_init (GstHLSDemux * demux)
|
|
{
|
|
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.
|
|
* Until the download helper can do estimates,
|
|
* use the main variant, or a video stream if the
|
|
* main variant stream is not loading */
|
|
|
|
/* 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);
|
|
}
|
|
}
|
|
|
|
/* Wait until the current variant playlist finishes loading, only
|
|
* for use when called from an external thread - seeking or initial
|
|
* manifest. From the scheduler task it will just hang */
|
|
static GstFlowReturn
|
|
gst_hls_demux_wait_for_variant_playlist (GstHLSDemux * hlsdemux)
|
|
{
|
|
GstFlowReturn flow_ret;
|
|
|
|
while ((flow_ret = gst_hls_demux_check_variant_playlist_loaded (hlsdemux)
|
|
== GST_ADAPTIVE_DEMUX_FLOW_BUSY)) {
|
|
if (!gst_adaptive_demux2_stream_wait_prepared (GST_ADAPTIVE_DEMUX2_STREAM
|
|
(hlsdemux->main_stream))) {
|
|
GST_DEBUG_OBJECT (hlsdemux,
|
|
"Interrupted waiting for stream to be prepared");
|
|
return GST_FLOW_FLUSHING;
|
|
}
|
|
}
|
|
|
|
return flow_ret;
|
|
}
|
|
|
|
#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) {
|
|
|
|
/* Switch to I-frame variant */
|
|
if (!gst_hls_demux_change_variant_playlist (hlsdemux, TRUE,
|
|
bitrate / ABS (rate), NULL))
|
|
return FALSE;
|
|
|
|
} else if (rate > -1.0 && rate <= 1.0 && (old_rate < -1.0 || old_rate > 1.0)) {
|
|
/* Switch to normal variant */
|
|
if (!gst_hls_demux_change_variant_playlist (hlsdemux, FALSE, bitrate, NULL))
|
|
return FALSE;
|
|
}
|
|
|
|
/* Of course the playlist isn't loaded as soon as we ask - we need to wait */
|
|
GstFlowReturn flow_ret = gst_hls_demux_wait_for_variant_playlist (hlsdemux);
|
|
if (flow_ret == GST_FLOW_FLUSHING)
|
|
return FALSE;
|
|
if (flow_ret != GST_FLOW_OK) {
|
|
GST_ELEMENT_ERROR (demux, STREAM, FAILED,
|
|
(_("Internal data stream error.")), ("Could not switch playlist"));
|
|
return FALSE;
|
|
}
|
|
|
|
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;
|
|
|
|
GstFlowReturn flow_ret;
|
|
|
|
while ((flow_ret =
|
|
gst_hls_demux_stream_seek (stream, rate >= 0, flags, target_pos,
|
|
¤t_pos) == GST_ADAPTIVE_DEMUX_FLOW_BUSY)) {
|
|
if (!gst_adaptive_demux2_stream_wait_prepared (GST_ADAPTIVE_DEMUX2_STREAM
|
|
(stream))) {
|
|
GST_DEBUG_OBJECT (hlsdemux,
|
|
"Interrupted waiting for stream to be prepared for seek");
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (flow_ret != 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 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_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;
|
|
|
|
/* 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;
|
|
|
|
gst_hls_demux_stream_set_playlist_uri (hlsdemux_stream,
|
|
demux->current_variant->uri);
|
|
gst_hls_demux_stream_start_playlist_loading (hlsdemux_stream);
|
|
}
|
|
|
|
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);
|
|
gst_hls_demux_stream_set_playlist_uri (media_stream, media->uri);
|
|
}
|
|
|
|
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) {
|
|
if (hlsdemux->pending_variant != variant) {
|
|
GST_DEBUG_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);
|
|
}
|
|
|
|
if (hlsdemux->main_stream) {
|
|
/* The variant stream exists, update the playlist we're loading */
|
|
gst_hls_demux_stream_set_playlist_uri (hlsdemux->main_stream, variant->uri);
|
|
}
|
|
}
|
|
|
|
/* Called to process the initial multi-variant (or simple playlist)
|
|
* received on the element's sinkpad */
|
|
static gboolean
|
|
gst_hls_demux_process_initial_manifest (GstAdaptiveDemux * demux,
|
|
GstBuffer * buf)
|
|
{
|
|
GstHLSVariantStream *variant;
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
gchar *playlist = NULL;
|
|
guint start_bitrate = hlsdemux->start_bitrate;
|
|
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);
|
|
}
|
|
|
|
if (start_bitrate == 0)
|
|
start_bitrate = demux->connection_speed;
|
|
|
|
/* select the initial variant stream */
|
|
if (start_bitrate > 0) {
|
|
variant =
|
|
gst_hls_master_playlist_get_variant_for_bitrate (hlsdemux->master,
|
|
FALSE, start_bitrate, demux->min_bitrate, hlsdemux->failed_variants);
|
|
} else {
|
|
variant = hlsdemux->master->default_variant;
|
|
}
|
|
|
|
if (variant == NULL) {
|
|
GST_ELEMENT_ERROR (demux, STREAM, FAILED,
|
|
(_("Internal data stream error.")),
|
|
("Could not find an initial variant to play"));
|
|
}
|
|
|
|
GST_INFO_OBJECT (hlsdemux,
|
|
"Manifest processed, initial variant selected : `%s`", variant->name);
|
|
gst_hls_demux_set_current_variant (hlsdemux, variant);
|
|
|
|
GST_DEBUG_OBJECT (hlsdemux, "Manifest handled, now setting up streams");
|
|
|
|
ret = gst_hls_demux_setup_streams (demux);
|
|
if (!ret)
|
|
return FALSE;
|
|
|
|
if (simple_media_playlist) {
|
|
GstM3U8SeekResult seek_result;
|
|
GstM3U8MediaSegment *segment;
|
|
|
|
hlsdemux->main_stream->playlist = simple_media_playlist;
|
|
/* This is the initial variant playlist. We will use it to base all our timing
|
|
* from. */
|
|
segment = g_ptr_array_index (simple_media_playlist->segments, 0);
|
|
if (segment) {
|
|
segment->stream_time = 0;
|
|
gst_hls_media_playlist_recalculate_stream_time (simple_media_playlist,
|
|
segment);
|
|
}
|
|
|
|
if (!gst_hls_media_playlist_get_starting_segment (simple_media_playlist,
|
|
&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_handle_variant_playlist_update (hlsdemux,
|
|
simple_media_playlist->uri, simple_media_playlist);
|
|
}
|
|
|
|
/* If this is a multi-variant playlist, wait for the initial variant playlist to load */
|
|
if (!hlsdemux->master->is_simple) {
|
|
GstFlowReturn flow_ret = gst_hls_demux_wait_for_variant_playlist (hlsdemux);
|
|
if (flow_ret == GST_FLOW_FLUSHING)
|
|
return FALSE;
|
|
if (flow_ret != GST_FLOW_OK) {
|
|
GST_ELEMENT_ERROR (demux, STREAM, FAILED,
|
|
(_("Internal data stream error.")),
|
|
("Could not fetch media playlist"));
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* Make sure the external manifest copy of the main playlist
|
|
* is available to the baseclass at the start */
|
|
gst_hls_demux_update_manifest (demux);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
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_playlist)
|
|
duration = gst_hls_media_playlist_get_duration (hlsdemux->main_playlist);
|
|
|
|
return duration;
|
|
}
|
|
|
|
/* Called from base class with the MANIFEST_LOCK held */
|
|
static gboolean
|
|
gst_hls_demux_is_live (GstAdaptiveDemux * demux)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
gboolean is_live = FALSE;
|
|
|
|
if (hlsdemux->main_playlist)
|
|
is_live = gst_hls_media_playlist_is_live (hlsdemux->main_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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Called by the base class with the manifest lock held */
|
|
static GstFlowReturn
|
|
gst_hls_demux_update_manifest (GstAdaptiveDemux * demux)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
|
|
/* Take a copy of the main variant playlist for base class
|
|
* calls that need access from outside the scheduler task,
|
|
* holding the MANIFEST_LOCK */
|
|
if (hlsdemux->main_stream && hlsdemux->main_stream->playlist) {
|
|
if (hlsdemux->main_playlist)
|
|
gst_hls_media_playlist_unref (hlsdemux->main_playlist);
|
|
hlsdemux->main_playlist =
|
|
gst_hls_media_playlist_ref (hlsdemux->main_stream->playlist);
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
return GST_ADAPTIVE_DEMUX_FLOW_BUSY;
|
|
}
|
|
|
|
void
|
|
gst_hls_demux_handle_variant_playlist_update (GstHLSDemux * demux,
|
|
const gchar * playlist_uri, GstHLSMediaPlaylist * playlist)
|
|
{
|
|
if (demux->main_stream == NULL || !demux->main_stream->playlist_fetched) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (demux->pending_variant) {
|
|
/* The pending variant must always match the one that just got updated:
|
|
* The loader should only do a callback for the most recently set URI */
|
|
g_assert (g_str_equal (demux->pending_variant->uri, playlist_uri));
|
|
|
|
gboolean changed = (demux->pending_variant != demux->current_variant);
|
|
|
|
gst_hls_variant_stream_unref (demux->current_variant);
|
|
/* Stealing ref */
|
|
demux->current_variant = demux->pending_variant;
|
|
demux->pending_variant = NULL;
|
|
|
|
if (changed) {
|
|
GstAdaptiveDemux *basedemux = GST_ADAPTIVE_DEMUX (demux);
|
|
const gchar *main_uri =
|
|
gst_adaptive_demux_get_manifest_ref_uri (basedemux);
|
|
gchar *uri = demux->current_variant->uri;
|
|
gint new_bandwidth = demux->current_variant->bandwidth;
|
|
|
|
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)));
|
|
|
|
/* Mark discont on the next packet after switching variant */
|
|
GST_ADAPTIVE_DEMUX2_STREAM (demux->main_stream)->discont = TRUE;
|
|
}
|
|
}
|
|
|
|
/* Update time mappings. We only use the variant stream for collecting
|
|
* mappings since it is the reference on which rendition stream timing will
|
|
* be based. */
|
|
gst_hls_update_time_mappings (demux, playlist);
|
|
gst_hls_media_playlist_dump (playlist);
|
|
|
|
/* Get the base class to call the update_manifest() vfunc with the MANIFEST_LOCK()
|
|
* held */
|
|
gst_adaptive_demux2_manual_manifest_update (GST_ADAPTIVE_DEMUX (demux));
|
|
}
|
|
|
|
void
|
|
gst_hls_demux_handle_variant_playlist_update_error (GstHLSDemux * demux,
|
|
const gchar * playlist_uri)
|
|
{
|
|
GST_DEBUG_OBJECT (demux, "Playlist update failure for variant URI %s",
|
|
playlist_uri);
|
|
|
|
/* Check if this is a new load of the pending variant, or a reload
|
|
* of the current variant */
|
|
GstHLSVariantStream *variant = demux->pending_variant;
|
|
if (variant == NULL)
|
|
variant = demux->current_variant;
|
|
|
|
/* If the variant has an fallback URIs available, we can try one of those */
|
|
if (variant->fallback != NULL) {
|
|
gchar *fallback_uri = (gchar *) (variant->fallback->data);
|
|
|
|
GST_DEBUG_OBJECT (demux,
|
|
"Variant playlist update failed. Switching to fallback URI %s",
|
|
fallback_uri);
|
|
|
|
variant->fallback = g_list_remove (variant->fallback, fallback_uri);
|
|
g_free (variant->uri);
|
|
variant->uri = fallback_uri;
|
|
|
|
if (demux->main_stream) {
|
|
/* The variant stream exists, update the playlist we're loading */
|
|
gst_hls_demux_stream_set_playlist_uri (demux->main_stream, variant->uri);
|
|
}
|
|
return;
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (demux, "Variant playlist update failed. "
|
|
"Marking variant URL %s as failed and switching over to another variant",
|
|
playlist_uri);
|
|
|
|
/* The variant must always match the one that just got updated:
|
|
* The loader should only do a callback for the most recently set URI */
|
|
g_assert (g_str_equal (variant->uri, playlist_uri));
|
|
|
|
/* If we didn't already add this playlist to the failed variants list
|
|
* do so now. It's possible we get an update error again if we failed
|
|
* to choose a new variant and posted error but didn't get shut down
|
|
* yet */
|
|
if (g_list_find (demux->failed_variants, variant) == NULL) {
|
|
demux->failed_variants =
|
|
g_list_prepend (demux->failed_variants,
|
|
gst_hls_variant_stream_ref (variant));
|
|
}
|
|
|
|
/* Now try to find another variant to play */
|
|
gdouble play_rate = gst_adaptive_demux_play_rate (GST_ADAPTIVE_DEMUX (demux));
|
|
guint64 bitrate = gst_hls_demux_get_bitrate (demux);
|
|
|
|
GST_DEBUG_OBJECT (demux, "Trying to find failover variant playlist");
|
|
|
|
if (!gst_hls_demux_change_variant_playlist (demux,
|
|
variant->iframe, bitrate / MAX (1.0, ABS (play_rate)), NULL)) {
|
|
GST_ERROR_OBJECT (demux, "Failed to choose a new variant to play");
|
|
GST_ELEMENT_ERROR (demux, STREAM, FAILED,
|
|
(_("Internal data stream error.")),
|
|
("Could not update any variant playlist"));
|
|
}
|
|
}
|
|
|
|
/* 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,
|
|
&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->main_playlist) {
|
|
gst_hls_media_playlist_unref (demux->main_playlist);
|
|
demux->main_playlist = 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;
|
|
}
|
|
if (demux->failed_variants != NULL) {
|
|
g_list_free_full (demux->failed_variants,
|
|
(GDestroyNotify) gst_hls_variant_stream_unref);
|
|
demux->failed_variants = 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_check_variant_playlist_loaded (GstHLSDemux * demux)
|
|
{
|
|
GstHLSVariantStream *target_variant =
|
|
demux->pending_variant ? demux->pending_variant : demux->current_variant;
|
|
GstHLSDemuxStream *stream = demux->main_stream;
|
|
|
|
return gst_hls_demux_stream_check_current_playlist_uri (stream,
|
|
target_variant->uri);
|
|
}
|
|
|
|
gboolean
|
|
gst_hls_demux_change_variant_playlist (GstHLSDemux * demux,
|
|
gboolean iframe_variant, guint max_bitrate, gboolean * changed)
|
|
{
|
|
GstAdaptiveDemux *adaptive_demux = GST_ADAPTIVE_DEMUX_CAST (demux);
|
|
|
|
if (changed)
|
|
*changed = FALSE;
|
|
|
|
/* Make sure we keep a reference for the debug output below */
|
|
GstHLSVariantStream *new_variant =
|
|
gst_hls_master_playlist_get_variant_for_bitrate (demux->master,
|
|
iframe_variant, max_bitrate, adaptive_demux->min_bitrate,
|
|
demux->failed_variants);
|
|
|
|
/* We're out of available variants to use */
|
|
if (new_variant == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
GstHLSVariantStream *previous_variant =
|
|
gst_hls_variant_stream_ref (demux->current_variant);
|
|
|
|
/* Don't do anything else if the playlist is the same */
|
|
if (new_variant == previous_variant) {
|
|
GST_TRACE_OBJECT (demux, "Variant didn't change from bandwidth %dbps",
|
|
new_variant->bandwidth);
|
|
gst_hls_variant_stream_unref (previous_variant);
|
|
return TRUE;
|
|
}
|
|
|
|
gst_hls_demux_set_current_variant (demux, new_variant);
|
|
|
|
gint new_bandwidth = new_variant->bandwidth;
|
|
|
|
GST_INFO_OBJECT (demux, "Client was on %dbps, max allowed is %dbps, switching"
|
|
" to bitrate %dbps", previous_variant->bandwidth, max_bitrate,
|
|
new_bandwidth);
|
|
|
|
gst_hls_variant_stream_unref (previous_variant);
|
|
if (changed)
|
|
*changed = TRUE;
|
|
return TRUE;
|
|
}
|
|
|
|
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_playlist) {
|
|
ret =
|
|
gst_hls_media_playlist_get_seek_range (hlsdemux->main_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;
|
|
}
|