mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-30 13:41:48 +00:00
3c5eae63c0
hlsdemux tries to find type if given buffer size is large enought to find type (currently the threshold is 2KB), or EOS in some cases. However, since there can be small byte fragments such as WebVTT, demux should try to find type at the end of a fragment https://bugzilla.gnome.org/show_bug.cgi?id=779011
1840 lines
56 KiB
C
1840 lines
56 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>
|
|
*
|
|
* 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-hlsdemux
|
|
*
|
|
* HTTP Live Streaming demuxer element.
|
|
*
|
|
* <refsect2>
|
|
* <title>Example launch line</title>
|
|
* |[
|
|
* gst-launch-1.0 souphttpsrc location=http://devimages.apple.com/iphone/samples/bipbop/gear4/prog_index.m3u8 ! hlsdemux ! decodebin ! videoconvert ! videoscale ! autovideosink
|
|
* ]|
|
|
* </refsect2>
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
|
|
#include <string.h>
|
|
#include <gst/base/gsttypefindhelper.h>
|
|
#include "gsthlsdemux.h"
|
|
|
|
static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src_%u",
|
|
GST_PAD_SRC,
|
|
GST_PAD_SOMETIMES,
|
|
GST_STATIC_CAPS_ANY);
|
|
|
|
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_demux_debug);
|
|
#define GST_CAT_DEFAULT gst_hls_demux_debug
|
|
|
|
#define GST_M3U8_CLIENT_LOCK(l) /* FIXME */
|
|
#define GST_M3U8_CLIENT_UNLOCK(l) /* FIXME */
|
|
|
|
/* GObject */
|
|
static void gst_hls_demux_finalize (GObject * obj);
|
|
|
|
/* GstElement */
|
|
static GstStateChangeReturn
|
|
gst_hls_demux_change_state (GstElement * element, GstStateChange transition);
|
|
|
|
/* GstHLSDemux */
|
|
static gboolean gst_hls_demux_update_playlist (GstHLSDemux * demux,
|
|
gboolean update, GError ** err);
|
|
static gchar *gst_hls_src_buf_to_utf8_playlist (GstBuffer * buf);
|
|
|
|
static gboolean gst_hls_demux_change_playlist (GstHLSDemux * demux,
|
|
guint max_bitrate, gboolean * changed);
|
|
static GstBuffer *gst_hls_demux_decrypt_fragment (GstHLSDemux * demux,
|
|
GstHLSDemuxStream * stream, GstBuffer * encrypted_buffer, GError ** err);
|
|
static gboolean
|
|
gst_hls_demux_stream_decrypt_start (GstHLSDemuxStream * stream,
|
|
const guint8 * key_data, const guint8 * iv_data);
|
|
static void gst_hls_demux_stream_decrypt_end (GstHLSDemuxStream * stream);
|
|
|
|
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 gboolean gst_hls_demux_seek (GstAdaptiveDemux * demux, GstEvent * seek);
|
|
static GstFlowReturn gst_hls_demux_stream_seek (GstAdaptiveDemuxStream *
|
|
stream, gboolean forward, GstSeekFlags flags, GstClockTime ts,
|
|
GstClockTime * final_ts);
|
|
static gboolean
|
|
gst_hls_demux_start_fragment (GstAdaptiveDemux * demux,
|
|
GstAdaptiveDemuxStream * stream);
|
|
static GstFlowReturn gst_hls_demux_finish_fragment (GstAdaptiveDemux * demux,
|
|
GstAdaptiveDemuxStream * stream);
|
|
static GstFlowReturn gst_hls_demux_data_received (GstAdaptiveDemux * demux,
|
|
GstAdaptiveDemuxStream * stream, GstBuffer * buffer);
|
|
static void gst_hls_demux_stream_free (GstAdaptiveDemuxStream * stream);
|
|
static gboolean gst_hls_demux_stream_has_next_fragment (GstAdaptiveDemuxStream *
|
|
stream);
|
|
static GstFlowReturn gst_hls_demux_advance_fragment (GstAdaptiveDemuxStream *
|
|
stream);
|
|
static GstFlowReturn gst_hls_demux_update_fragment_info (GstAdaptiveDemuxStream
|
|
* stream);
|
|
static gboolean gst_hls_demux_select_bitrate (GstAdaptiveDemuxStream * stream,
|
|
guint64 bitrate);
|
|
static void gst_hls_demux_reset (GstAdaptiveDemux * demux);
|
|
static gboolean gst_hls_demux_get_live_seek_range (GstAdaptiveDemux * demux,
|
|
gint64 * start, gint64 * stop);
|
|
static GstM3U8 *gst_hls_demux_stream_get_m3u8 (GstHLSDemuxStream * hls_stream);
|
|
static void gst_hls_demux_set_current_variant (GstHLSDemux * hlsdemux,
|
|
GstHLSVariantStream * variant);
|
|
|
|
#define gst_hls_demux_parent_class parent_class
|
|
G_DEFINE_TYPE (GstHLSDemux, gst_hls_demux, GST_TYPE_ADAPTIVE_DEMUX);
|
|
|
|
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_class_init (GstHLSDemuxClass * klass)
|
|
{
|
|
GObjectClass *gobject_class;
|
|
GstElementClass *element_class;
|
|
GstAdaptiveDemuxClass *adaptivedemux_class;
|
|
|
|
gobject_class = (GObjectClass *) klass;
|
|
element_class = (GstElementClass *) klass;
|
|
adaptivedemux_class = (GstAdaptiveDemuxClass *) klass;
|
|
|
|
gobject_class->finalize = gst_hls_demux_finalize;
|
|
|
|
element_class->change_state = GST_DEBUG_FUNCPTR (gst_hls_demux_change_state);
|
|
|
|
gst_element_class_add_static_pad_template (element_class, &srctemplate);
|
|
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",
|
|
"Marc-Andre Lureau <marcandre.lureau@gmail.com>\n"
|
|
"Andoni Morales Alastruey <ylatuya@gmail.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;
|
|
adaptivedemux_class->stream_seek = gst_hls_demux_stream_seek;
|
|
adaptivedemux_class->stream_has_next_fragment =
|
|
gst_hls_demux_stream_has_next_fragment;
|
|
adaptivedemux_class->stream_advance_fragment = gst_hls_demux_advance_fragment;
|
|
adaptivedemux_class->stream_update_fragment_info =
|
|
gst_hls_demux_update_fragment_info;
|
|
adaptivedemux_class->stream_select_bitrate = gst_hls_demux_select_bitrate;
|
|
adaptivedemux_class->stream_free = gst_hls_demux_stream_free;
|
|
|
|
adaptivedemux_class->start_fragment = gst_hls_demux_start_fragment;
|
|
adaptivedemux_class->finish_fragment = gst_hls_demux_finish_fragment;
|
|
adaptivedemux_class->data_received = gst_hls_demux_data_received;
|
|
|
|
GST_DEBUG_CATEGORY_INIT (gst_hls_demux_debug, "hlsdemux", 0,
|
|
"hlsdemux element");
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_init (GstHLSDemux * demux)
|
|
{
|
|
gst_adaptive_demux_set_stream_struct_size (GST_ADAPTIVE_DEMUX_CAST (demux),
|
|
sizeof (GstHLSDemuxStream));
|
|
|
|
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 GstPad *
|
|
gst_hls_demux_create_pad (GstHLSDemux * hlsdemux)
|
|
{
|
|
gchar *name;
|
|
GstPad *pad;
|
|
|
|
name = g_strdup_printf ("src_%u", hlsdemux->srcpad_counter++);
|
|
pad = gst_pad_new_from_static_template (&srctemplate, name);
|
|
g_free (name);
|
|
|
|
return pad;
|
|
}
|
|
|
|
static guint64
|
|
gst_hls_demux_get_bitrate (GstHLSDemux * hlsdemux)
|
|
{
|
|
GstAdaptiveDemux *demux = GST_ADAPTIVE_DEMUX_CAST (hlsdemux);
|
|
|
|
/* Valid because hlsdemux only has a single output */
|
|
if (demux->streams) {
|
|
GstAdaptiveDemuxStream *stream = demux->streams->data;
|
|
return stream->current_download_rate;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_stream_clear_pending_data (GstHLSDemuxStream * hls_stream)
|
|
{
|
|
if (hls_stream->pending_encrypted_data)
|
|
gst_adapter_clear (hls_stream->pending_encrypted_data);
|
|
gst_buffer_replace (&hls_stream->pending_decrypted_buffer, NULL);
|
|
gst_buffer_replace (&hls_stream->pending_typefind_buffer, NULL);
|
|
gst_buffer_replace (&hls_stream->pending_pcr_buffer, NULL);
|
|
hls_stream->current_offset = -1;
|
|
gst_hls_demux_stream_decrypt_end (hls_stream);
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_clear_all_pending_data (GstHLSDemux * hlsdemux)
|
|
{
|
|
GstAdaptiveDemux *demux = (GstAdaptiveDemux *) hlsdemux;
|
|
GList *walk;
|
|
|
|
for (walk = demux->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);
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static void
|
|
gst_hls_demux_set_current (GstHLSDemux * self, GstM3U8 * m3u8)
|
|
{
|
|
GST_M3U8_CLIENT_LOCK (self);
|
|
if (m3u8 != self->current) {
|
|
self->current = m3u8;
|
|
self->current->duration = GST_CLOCK_TIME_NONE;
|
|
self->current->current_file = NULL;
|
|
|
|
#if 0
|
|
// FIXME: this makes no sense after we just set self->current=m3u8 above (tpm)
|
|
// also, these values don't necessarily align between different lists
|
|
m3u8->current_file_duration = self->current->current_file_duration;
|
|
m3u8->sequence = self->current->sequence;
|
|
m3u8->sequence_position = self->current->sequence_position;
|
|
m3u8->highest_sequence_number = self->current->highest_sequence_number;
|
|
m3u8->first_file_start = self->current->first_file_start;
|
|
m3u8->last_file_end = self->current->last_file_end;
|
|
#endif
|
|
}
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
}
|
|
#endif
|
|
|
|
#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;
|
|
GstClockTime 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);
|
|
gst_uri_downloader_reset (demux->downloader);
|
|
if (!gst_hls_demux_update_playlist (hlsdemux, FALSE, &err)) {
|
|
GST_ELEMENT_ERROR_FROM_ERROR (hlsdemux, "Could not switch playlist", err);
|
|
return FALSE;
|
|
}
|
|
//hlsdemux->discont = TRUE;
|
|
|
|
gst_hls_demux_change_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);
|
|
gst_uri_downloader_reset (demux->downloader);
|
|
if (!gst_hls_demux_update_playlist (hlsdemux, FALSE, &err)) {
|
|
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_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);
|
|
}
|
|
|
|
for (walk = demux->streams; walk; walk = g_list_next (walk)) {
|
|
GstAdaptiveDemuxStream *stream =
|
|
GST_ADAPTIVE_DEMUX_STREAM_CAST (walk->data);
|
|
|
|
gst_hls_demux_stream_seek (stream, rate >= 0, flags, target_pos,
|
|
¤t_pos);
|
|
|
|
/* 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_stream_seek (GstAdaptiveDemuxStream * stream, gboolean forward,
|
|
GstSeekFlags flags, GstClockTime ts, GstClockTime * final_ts)
|
|
{
|
|
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
GList *walk;
|
|
GstClockTime current_pos;
|
|
gint64 current_sequence;
|
|
gboolean snap_after, snap_nearest;
|
|
GstM3U8MediaFile *file = NULL;
|
|
|
|
current_sequence = 0;
|
|
current_pos = gst_m3u8_is_live (hls_stream->playlist) ?
|
|
hls_stream->playlist->first_file_start : 0;
|
|
|
|
/* Snap to segment boundary. Improves seek performance on slow machines. */
|
|
snap_nearest =
|
|
(flags & GST_SEEK_FLAG_SNAP_NEAREST) == GST_SEEK_FLAG_SNAP_NEAREST;
|
|
snap_after = ! !(flags & GST_SEEK_FLAG_SNAP_AFTER);
|
|
|
|
GST_M3U8_CLIENT_LOCK (hlsdemux->client);
|
|
/* FIXME: Here we need proper discont handling */
|
|
for (walk = hls_stream->playlist->files; walk; walk = walk->next) {
|
|
file = walk->data;
|
|
|
|
current_sequence = file->sequence;
|
|
if ((forward && snap_after) || snap_nearest) {
|
|
if (current_pos >= ts)
|
|
break;
|
|
if (snap_nearest && ts - current_pos < file->duration / 2)
|
|
break;
|
|
} else if (!forward && snap_after) {
|
|
/* check if the next fragment is our target, in this case we want to
|
|
* start from the previous fragment */
|
|
GstClockTime next_pos = current_pos + file->duration;
|
|
|
|
if (next_pos <= ts && ts < next_pos + file->duration) {
|
|
break;
|
|
}
|
|
} else if (current_pos <= ts && ts < current_pos + file->duration) {
|
|
break;
|
|
}
|
|
current_pos += file->duration;
|
|
}
|
|
|
|
if (walk == NULL) {
|
|
GST_DEBUG_OBJECT (stream->pad, "seeking further than track duration");
|
|
current_sequence++;
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (stream->pad, "seeking to sequence %u",
|
|
(guint) current_sequence);
|
|
hls_stream->reset_pts = TRUE;
|
|
hls_stream->playlist->sequence = current_sequence;
|
|
hls_stream->playlist->current_file = walk;
|
|
hls_stream->playlist->sequence_position = current_pos;
|
|
GST_M3U8_CLIENT_UNLOCK (hlsdemux->client);
|
|
|
|
/* Play from the end of the current selected segment */
|
|
if (file) {
|
|
if (!forward && IS_SNAP_SEEK (flags))
|
|
current_pos += file->duration;
|
|
}
|
|
|
|
/* update stream's segment position */
|
|
stream->segment.position = current_pos;
|
|
|
|
if (final_ts)
|
|
*final_ts = current_pos;
|
|
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_hls_demux_update_manifest (GstAdaptiveDemux * demux)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
if (!gst_hls_demux_update_playlist (hlsdemux, TRUE, NULL))
|
|
return GST_FLOW_ERROR;
|
|
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
static void
|
|
create_stream_for_playlist (GstAdaptiveDemux * demux, GstM3U8 * playlist,
|
|
gboolean is_primary_playlist, gboolean selected)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
GstHLSDemuxStream *hlsdemux_stream;
|
|
GstAdaptiveDemuxStream *stream;
|
|
|
|
if (!selected) {
|
|
/* FIXME: Later, create the stream but mark not-selected */
|
|
GST_LOG_OBJECT (demux, "Ignoring not-selected stream");
|
|
return;
|
|
}
|
|
|
|
stream = gst_adaptive_demux_stream_new (demux,
|
|
gst_hls_demux_create_pad (hlsdemux));
|
|
|
|
hlsdemux_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
|
|
hlsdemux_stream->stream_type = GST_HLS_TSREADER_NONE;
|
|
|
|
hlsdemux_stream->playlist = gst_m3u8_ref (playlist);
|
|
hlsdemux_stream->is_primary_playlist = is_primary_playlist;
|
|
|
|
hlsdemux_stream->do_typefind = TRUE;
|
|
hlsdemux_stream->reset_pts = TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_setup_streams (GstAdaptiveDemux * demux)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
GstHLSVariantStream *playlist = hlsdemux->current_variant;
|
|
gint i;
|
|
|
|
if (playlist == NULL) {
|
|
GST_WARNING_OBJECT (demux, "Can't configure streams - no variant selected");
|
|
return FALSE;
|
|
}
|
|
|
|
gst_hls_demux_clear_all_pending_data (hlsdemux);
|
|
|
|
/* 1 output for the main playlist */
|
|
create_stream_for_playlist (demux, playlist->m3u8, TRUE, TRUE);
|
|
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
GList *mlist = playlist->media[i];
|
|
while (mlist != NULL) {
|
|
GstHLSMedia *media = mlist->data;
|
|
|
|
if (media->uri == NULL /* || media->mtype != GST_HLS_MEDIA_TYPE_AUDIO */ ) {
|
|
/* No uri means this is a placeholder for a stream
|
|
* contained in another mux */
|
|
GST_LOG_OBJECT (demux, "Skipping stream %s type %d with no URI",
|
|
media->name, media->mtype);
|
|
mlist = mlist->next;
|
|
continue;
|
|
}
|
|
GST_LOG_OBJECT (demux, "media of type %d - %s, uri: %s", i,
|
|
media->name, media->uri);
|
|
create_stream_for_playlist (demux, media->playlist, FALSE,
|
|
(media->mtype == GST_HLS_MEDIA_TYPE_VIDEO ||
|
|
media->mtype == GST_HLS_MEDIA_TYPE_AUDIO));
|
|
|
|
mlist = mlist->next;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static const gchar *
|
|
gst_adaptive_demux_get_manifest_ref_uri (GstAdaptiveDemux * d)
|
|
{
|
|
return d->manifest_base_uri ? d->manifest_base_uri : d->manifest_uri;
|
|
}
|
|
|
|
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) {
|
|
gint i;
|
|
|
|
//#warning FIXME: Synching fragments across variants
|
|
// should be done based on media timestamps, and
|
|
// discont-sequence-numbers not sequence numbers.
|
|
variant->m3u8->sequence_position =
|
|
hlsdemux->current_variant->m3u8->sequence_position;
|
|
variant->m3u8->sequence = hlsdemux->current_variant->m3u8->sequence;
|
|
|
|
GST_DEBUG_OBJECT (hlsdemux,
|
|
"Switching Variant. Copying over sequence %" G_GINT64_FORMAT
|
|
" and sequence_pos %" GST_TIME_FORMAT, variant->m3u8->sequence,
|
|
GST_TIME_ARGS (variant->m3u8->sequence_position));
|
|
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
GList *mlist = hlsdemux->current_variant->media[i];
|
|
|
|
while (mlist != NULL) {
|
|
GstHLSMedia *old_media = mlist->data;
|
|
GstHLSMedia *new_media =
|
|
gst_hls_variant_find_matching_media (variant, old_media);
|
|
|
|
if (new_media) {
|
|
new_media->playlist->sequence = old_media->playlist->sequence;
|
|
new_media->playlist->sequence_position =
|
|
old_media->playlist->sequence_position;
|
|
}
|
|
mlist = mlist->next;
|
|
}
|
|
}
|
|
|
|
gst_hls_variant_stream_unref (hlsdemux->current_variant);
|
|
}
|
|
|
|
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;
|
|
|
|
GST_INFO_OBJECT (demux, "Initial playlist location: %s (base uri: %s)",
|
|
demux->manifest_uri, demux->manifest_base_uri);
|
|
|
|
playlist = gst_hls_src_buf_to_utf8_playlist (buf);
|
|
if (playlist == NULL) {
|
|
GST_WARNING_OBJECT (demux, "Error validating initial playlist");
|
|
return FALSE;
|
|
}
|
|
|
|
GST_M3U8_CLIENT_LOCK (self);
|
|
hlsdemux->master = gst_hls_master_playlist_new_from_data (playlist,
|
|
gst_adaptive_demux_get_manifest_ref_uri (demux));
|
|
|
|
if (hlsdemux->master == NULL || hlsdemux->master->variants == 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."));
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
|
|
/* select the initial variant stream */
|
|
if (demux->connection_speed == 0) {
|
|
variant = hlsdemux->master->default_variant;
|
|
} else {
|
|
variant =
|
|
gst_hls_master_playlist_get_variant_for_bitrate (hlsdemux->master,
|
|
NULL, demux->connection_speed);
|
|
}
|
|
|
|
if (variant) {
|
|
GST_INFO_OBJECT (hlsdemux, "selected %s", variant->name);
|
|
gst_hls_demux_set_current_variant (hlsdemux, variant); // FIXME: inline?
|
|
}
|
|
|
|
/* get the selected media playlist (unless the inital list was one already) */
|
|
if (!hlsdemux->master->is_simple) {
|
|
GError *err = NULL;
|
|
|
|
if (!gst_hls_demux_update_playlist (hlsdemux, FALSE, &err)) {
|
|
GST_ELEMENT_ERROR_FROM_ERROR (demux, "Could not fetch media playlist",
|
|
err);
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
return FALSE;
|
|
}
|
|
}
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
|
|
return gst_hls_demux_setup_streams (demux);
|
|
}
|
|
|
|
static GstClockTime
|
|
gst_hls_demux_get_duration (GstAdaptiveDemux * demux)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
GstClockTime duration = GST_CLOCK_TIME_NONE;
|
|
|
|
if (hlsdemux->current_variant != NULL)
|
|
duration = gst_m3u8_get_duration (hlsdemux->current_variant->m3u8);
|
|
|
|
return duration;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_is_live (GstAdaptiveDemux * demux)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
gboolean is_live = FALSE;
|
|
|
|
if (hlsdemux->current_variant)
|
|
is_live = gst_hls_variant_stream_is_live (hlsdemux->current_variant);
|
|
|
|
return is_live;
|
|
}
|
|
|
|
static const GstHLSKey *
|
|
gst_hls_demux_get_key (GstHLSDemux * demux, const gchar * key_url,
|
|
const gchar * referer, gboolean allow_cache)
|
|
{
|
|
GstFragment *key_fragment;
|
|
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);
|
|
|
|
key_fragment =
|
|
gst_uri_downloader_fetch_uri (GST_ADAPTIVE_DEMUX (demux)->downloader,
|
|
key_url, referer, FALSE, FALSE, allow_cache, &err);
|
|
|
|
if (key_fragment == 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 = gst_fragment_get_buffer (key_fragment);
|
|
|
|
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);
|
|
g_object_unref (key_fragment);
|
|
|
|
out:
|
|
|
|
g_mutex_unlock (&demux->keys_lock);
|
|
|
|
if (key != NULL)
|
|
GST_MEMDUMP_OBJECT (demux, "Key", key->data, 16);
|
|
|
|
return key;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_start_fragment (GstAdaptiveDemux * demux,
|
|
GstAdaptiveDemuxStream * stream)
|
|
{
|
|
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
const GstHLSKey *key;
|
|
GstM3U8 *m3u8;
|
|
|
|
gst_hls_demux_stream_clear_pending_data (hls_stream);
|
|
|
|
/* Init the timestamp reader for this fragment */
|
|
gst_hlsdemux_tsreader_init (&hls_stream->tsreader);
|
|
/* Reset the stream type if we already know it */
|
|
gst_hlsdemux_tsreader_set_type (&hls_stream->tsreader,
|
|
hls_stream->stream_type);
|
|
|
|
/* If no decryption is needed, there's nothing to be done here */
|
|
if (hls_stream->current_key == NULL)
|
|
return TRUE;
|
|
|
|
m3u8 = gst_hls_demux_stream_get_m3u8 (hls_stream);
|
|
|
|
key = gst_hls_demux_get_key (hlsdemux, hls_stream->current_key,
|
|
m3u8->uri, m3u8->allowcache);
|
|
|
|
if (key == NULL)
|
|
goto key_failed;
|
|
|
|
gst_hls_demux_stream_decrypt_start (hls_stream, key->data,
|
|
hls_stream->current_iv);
|
|
|
|
return TRUE;
|
|
|
|
key_failed:
|
|
{
|
|
GST_ELEMENT_ERROR (demux, STREAM, DEMUX,
|
|
("Couldn't retrieve key for decryption"), (NULL));
|
|
GST_WARNING_OBJECT (demux, "Failed to decrypt data");
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
static GstHLSTSReaderType
|
|
caps_to_reader (const GstCaps * caps)
|
|
{
|
|
const GstStructure *s = gst_caps_get_structure (caps, 0);
|
|
|
|
if (gst_structure_has_name (s, "video/mpegts"))
|
|
return GST_HLS_TSREADER_MPEGTS;
|
|
if (gst_structure_has_name (s, "application/x-id3"))
|
|
return GST_HLS_TSREADER_ID3;
|
|
|
|
return GST_HLS_TSREADER_NONE;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_hls_demux_handle_buffer (GstAdaptiveDemux * demux,
|
|
GstAdaptiveDemuxStream * stream, GstBuffer * buffer, gboolean at_eos)
|
|
{
|
|
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream); // FIXME: pass HlsStream into function
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
GstMapInfo info;
|
|
GstClockTime first_pcr, last_pcr;
|
|
GstTagList *tags;
|
|
|
|
if (buffer == NULL)
|
|
return GST_FLOW_OK;
|
|
|
|
gst_buffer_map (buffer, &info, GST_MAP_READ);
|
|
|
|
if (G_UNLIKELY (hls_stream->do_typefind)) {
|
|
GstCaps *caps = NULL;
|
|
guint buffer_size;
|
|
GstTypeFindProbability prob = GST_TYPE_FIND_NONE;
|
|
|
|
if (hls_stream->pending_typefind_buffer)
|
|
buffer = gst_buffer_append (hls_stream->pending_typefind_buffer, buffer);
|
|
hls_stream->pending_typefind_buffer = NULL;
|
|
|
|
gst_buffer_map (buffer, &info, GST_MAP_READ);
|
|
buffer_size = info.size;
|
|
|
|
/* Typefind could miss if buffer is too small. In this case we
|
|
* will retry later */
|
|
if (buffer_size >= (2 * 1024) || at_eos) {
|
|
caps =
|
|
gst_type_find_helper_for_data (GST_OBJECT_CAST (hlsdemux), info.data,
|
|
info.size, &prob);
|
|
}
|
|
|
|
if (G_UNLIKELY (!caps)) {
|
|
/* Won't need this mapping any more all paths return inside this if() */
|
|
gst_buffer_unmap (buffer, &info);
|
|
|
|
/* Only fail typefinding if we already a good amount of data
|
|
* and we still don't know the type */
|
|
if (buffer_size > (2 * 1024 * 1024) || at_eos) {
|
|
GST_ELEMENT_ERROR (hlsdemux, STREAM, TYPE_NOT_FOUND,
|
|
("Could not determine type of stream"), (NULL));
|
|
gst_buffer_unref (buffer);
|
|
return GST_FLOW_NOT_NEGOTIATED;
|
|
}
|
|
|
|
hls_stream->pending_typefind_buffer = buffer;
|
|
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (hlsdemux, "Typefind result: %" GST_PTR_FORMAT " prob:%d",
|
|
caps, prob);
|
|
|
|
hls_stream->stream_type = caps_to_reader (caps);
|
|
gst_hlsdemux_tsreader_set_type (&hls_stream->tsreader,
|
|
hls_stream->stream_type);
|
|
|
|
gst_adaptive_demux_stream_set_caps (stream, caps);
|
|
|
|
hls_stream->do_typefind = FALSE;
|
|
}
|
|
g_assert (hls_stream->pending_typefind_buffer == NULL);
|
|
|
|
gst_buffer_unmap (buffer, &info);
|
|
|
|
// Accumulate this buffer
|
|
if (hls_stream->pending_pcr_buffer) {
|
|
buffer = gst_buffer_append (hls_stream->pending_pcr_buffer, buffer);
|
|
hls_stream->pending_pcr_buffer = NULL;
|
|
}
|
|
|
|
if (!gst_hlsdemux_tsreader_find_pcrs (&hls_stream->tsreader, &buffer,
|
|
&first_pcr, &last_pcr, &tags)
|
|
&& !at_eos) {
|
|
// Store this buffer for later
|
|
hls_stream->pending_pcr_buffer = buffer;
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
if (tags) {
|
|
gst_adaptive_demux_stream_set_tags (stream, tags);
|
|
/* run typefind again on the trimmed buffer */
|
|
hls_stream->do_typefind = TRUE;
|
|
return gst_hls_demux_handle_buffer (demux, stream, buffer, at_eos);
|
|
}
|
|
|
|
if (buffer) {
|
|
buffer = gst_buffer_make_writable (buffer);
|
|
GST_BUFFER_OFFSET (buffer) = hls_stream->current_offset;
|
|
hls_stream->current_offset += gst_buffer_get_size (buffer);
|
|
GST_BUFFER_OFFSET_END (buffer) = hls_stream->current_offset;
|
|
return gst_adaptive_demux_stream_push_buffer (stream, buffer);
|
|
}
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_hls_demux_finish_fragment (GstAdaptiveDemux * demux,
|
|
GstAdaptiveDemuxStream * stream)
|
|
{
|
|
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream); // FIXME: pass HlsStream into function
|
|
GstFlowReturn ret = GST_FLOW_OK;
|
|
|
|
if (hls_stream->current_key)
|
|
gst_hls_demux_stream_decrypt_end (hls_stream);
|
|
|
|
if (stream->last_ret == GST_FLOW_OK) {
|
|
if (hls_stream->pending_decrypted_buffer) {
|
|
if (hls_stream->current_key) {
|
|
GstMapInfo info;
|
|
gssize unpadded_size;
|
|
|
|
/* Handle pkcs7 unpadding here */
|
|
gst_buffer_map (hls_stream->pending_decrypted_buffer, &info,
|
|
GST_MAP_READ);
|
|
unpadded_size = info.size - info.data[info.size - 1];
|
|
gst_buffer_unmap (hls_stream->pending_decrypted_buffer, &info);
|
|
|
|
gst_buffer_resize (hls_stream->pending_decrypted_buffer, 0,
|
|
unpadded_size);
|
|
}
|
|
|
|
ret =
|
|
gst_hls_demux_handle_buffer (demux, stream,
|
|
hls_stream->pending_decrypted_buffer, TRUE);
|
|
hls_stream->pending_decrypted_buffer = NULL;
|
|
}
|
|
|
|
if (ret == GST_FLOW_OK || ret == GST_FLOW_NOT_LINKED) {
|
|
if (G_UNLIKELY (hls_stream->pending_typefind_buffer)) {
|
|
GstBuffer *buf = hls_stream->pending_typefind_buffer;
|
|
hls_stream->pending_typefind_buffer = NULL;
|
|
|
|
gst_hls_demux_handle_buffer (demux, stream, buf, TRUE);
|
|
}
|
|
|
|
if (hls_stream->pending_pcr_buffer) {
|
|
GstBuffer *buf = hls_stream->pending_pcr_buffer;
|
|
hls_stream->pending_pcr_buffer = NULL;
|
|
|
|
ret = gst_hls_demux_handle_buffer (demux, stream, buf, TRUE);
|
|
}
|
|
|
|
GST_LOG_OBJECT (stream,
|
|
"Fragment PCRs were %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (hls_stream->tsreader.first_pcr),
|
|
GST_TIME_ARGS (hls_stream->tsreader.last_pcr));
|
|
}
|
|
}
|
|
|
|
gst_hls_demux_stream_clear_pending_data (hls_stream);
|
|
|
|
if (ret == GST_FLOW_OK || ret == GST_FLOW_NOT_LINKED)
|
|
return gst_adaptive_demux_stream_advance_fragment (demux, stream,
|
|
stream->fragment.duration);
|
|
return ret;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_hls_demux_data_received (GstAdaptiveDemux * demux,
|
|
GstAdaptiveDemuxStream * stream, GstBuffer * buffer)
|
|
{
|
|
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
|
|
if (hls_stream->current_offset == -1)
|
|
hls_stream->current_offset = 0;
|
|
|
|
/* Is it encrypted? */
|
|
if (hls_stream->current_key) {
|
|
GError *err = NULL;
|
|
gsize size;
|
|
GstBuffer *tmp_buffer;
|
|
|
|
if (hls_stream->pending_encrypted_data == NULL)
|
|
hls_stream->pending_encrypted_data = gst_adapter_new ();
|
|
|
|
gst_adapter_push (hls_stream->pending_encrypted_data, buffer);
|
|
size = gst_adapter_available (hls_stream->pending_encrypted_data);
|
|
|
|
/* must be a multiple of 16 */
|
|
size &= (~0xF);
|
|
|
|
if (size == 0) {
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
buffer = gst_adapter_take_buffer (hls_stream->pending_encrypted_data, size);
|
|
buffer =
|
|
gst_hls_demux_decrypt_fragment (hlsdemux, hls_stream, buffer, &err);
|
|
if (buffer == NULL) {
|
|
GST_ELEMENT_ERROR (demux, STREAM, DECODE, ("Failed to decrypt buffer"),
|
|
("decryption failed %s", err->message));
|
|
g_error_free (err);
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
|
|
tmp_buffer = hls_stream->pending_decrypted_buffer;
|
|
hls_stream->pending_decrypted_buffer = buffer;
|
|
buffer = tmp_buffer;
|
|
}
|
|
|
|
return gst_hls_demux_handle_buffer (demux, stream, buffer, FALSE);
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_stream_free (GstAdaptiveDemuxStream * stream)
|
|
{
|
|
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
|
|
if (hls_stream->playlist) {
|
|
gst_m3u8_unref (hls_stream->playlist);
|
|
hls_stream->playlist = NULL;
|
|
}
|
|
|
|
if (hls_stream->pending_encrypted_data)
|
|
g_object_unref (hls_stream->pending_encrypted_data);
|
|
|
|
gst_buffer_replace (&hls_stream->pending_decrypted_buffer, NULL);
|
|
gst_buffer_replace (&hls_stream->pending_typefind_buffer, NULL);
|
|
gst_buffer_replace (&hls_stream->pending_pcr_buffer, NULL);
|
|
|
|
if (hls_stream->current_key) {
|
|
g_free (hls_stream->current_key);
|
|
hls_stream->current_key = NULL;
|
|
}
|
|
if (hls_stream->current_iv) {
|
|
g_free (hls_stream->current_iv);
|
|
hls_stream->current_iv = NULL;
|
|
}
|
|
gst_hls_demux_stream_decrypt_end (hls_stream);
|
|
}
|
|
|
|
static GstM3U8 *
|
|
gst_hls_demux_stream_get_m3u8 (GstHLSDemuxStream * hlsdemux_stream)
|
|
{
|
|
GstM3U8 *m3u8;
|
|
|
|
m3u8 = hlsdemux_stream->playlist;
|
|
|
|
return m3u8;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_stream_has_next_fragment (GstAdaptiveDemuxStream * stream)
|
|
{
|
|
gboolean has_next;
|
|
GstM3U8 *m3u8;
|
|
|
|
m3u8 = gst_hls_demux_stream_get_m3u8 (GST_HLS_DEMUX_STREAM_CAST (stream));
|
|
|
|
has_next = gst_m3u8_has_next_fragment (m3u8, stream->demux->segment.rate > 0);
|
|
|
|
return has_next;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_hls_demux_advance_fragment (GstAdaptiveDemuxStream * stream)
|
|
{
|
|
GstHLSDemuxStream *hlsdemux_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
GstM3U8 *m3u8;
|
|
|
|
m3u8 = gst_hls_demux_stream_get_m3u8 (hlsdemux_stream);
|
|
|
|
gst_m3u8_advance_fragment (m3u8, stream->demux->segment.rate > 0);
|
|
hlsdemux_stream->reset_pts = FALSE;
|
|
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_hls_demux_update_fragment_info (GstAdaptiveDemuxStream * stream)
|
|
{
|
|
GstHLSDemuxStream *hlsdemux_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (stream->demux);
|
|
GstM3U8MediaFile *file;
|
|
GstClockTime sequence_pos;
|
|
gboolean discont, forward;
|
|
GstM3U8 *m3u8;
|
|
|
|
m3u8 = gst_hls_demux_stream_get_m3u8 (hlsdemux_stream);
|
|
|
|
forward = (stream->demux->segment.rate > 0);
|
|
file = gst_m3u8_get_next_fragment (m3u8, forward, &sequence_pos, &discont);
|
|
|
|
if (file == NULL) {
|
|
GST_INFO_OBJECT (hlsdemux, "This playlist doesn't contain more fragments");
|
|
return GST_FLOW_EOS;
|
|
}
|
|
|
|
if (stream->discont)
|
|
discont = TRUE;
|
|
|
|
/* set up our source for download */
|
|
if (hlsdemux_stream->reset_pts || discont
|
|
|| stream->demux->segment.rate < 0.0) {
|
|
stream->fragment.timestamp = sequence_pos;
|
|
} else {
|
|
stream->fragment.timestamp = GST_CLOCK_TIME_NONE;
|
|
}
|
|
|
|
g_free (hlsdemux_stream->current_key);
|
|
hlsdemux_stream->current_key = g_strdup (file->key);
|
|
g_free (hlsdemux_stream->current_iv);
|
|
hlsdemux_stream->current_iv = g_memdup (file->iv, sizeof (file->iv));
|
|
|
|
g_free (stream->fragment.uri);
|
|
stream->fragment.uri = g_strdup (file->uri);
|
|
|
|
GST_DEBUG_OBJECT (hlsdemux, "Stream %p URI now %s", stream, file->uri);
|
|
|
|
stream->fragment.range_start = file->offset;
|
|
if (file->size != -1)
|
|
stream->fragment.range_end = file->offset + file->size - 1;
|
|
else
|
|
stream->fragment.range_end = -1;
|
|
|
|
stream->fragment.duration = file->duration;
|
|
|
|
if (discont)
|
|
stream->discont = TRUE;
|
|
|
|
gst_m3u8_media_file_unref (file);
|
|
|
|
return GST_FLOW_OK;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_select_bitrate (GstAdaptiveDemuxStream * stream, guint64 bitrate)
|
|
{
|
|
GstAdaptiveDemux *demux = GST_ADAPTIVE_DEMUX_CAST (stream->demux);
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (stream->demux);
|
|
GstHLSDemuxStream *hls_stream = GST_HLS_DEMUX_STREAM_CAST (stream);
|
|
|
|
gboolean changed = FALSE;
|
|
|
|
GST_M3U8_CLIENT_LOCK (hlsdemux->client);
|
|
if (hlsdemux->master == NULL || hlsdemux->master->is_simple) {
|
|
GST_M3U8_CLIENT_UNLOCK (hlsdemux->client);
|
|
return FALSE;
|
|
}
|
|
GST_M3U8_CLIENT_UNLOCK (hlsdemux->client);
|
|
|
|
if (hls_stream->is_primary_playlist == FALSE) {
|
|
GST_LOG_OBJECT (hlsdemux,
|
|
"Stream %p Not choosing new bitrate - not the primary stream", stream);
|
|
return FALSE;
|
|
}
|
|
|
|
gst_hls_demux_change_playlist (hlsdemux, bitrate / MAX (1.0,
|
|
ABS (demux->segment.rate)), &changed);
|
|
if (changed)
|
|
gst_hls_demux_setup_streams (GST_ADAPTIVE_DEMUX_CAST (hlsdemux));
|
|
return changed;
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_reset (GstAdaptiveDemux * ademux)
|
|
{
|
|
GstHLSDemux *demux = GST_HLS_DEMUX_CAST (ademux);
|
|
|
|
GST_M3U8_CLIENT_LOCK (hlsdemux->client);
|
|
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;
|
|
}
|
|
demux->srcpad_counter = 0;
|
|
|
|
gst_hls_demux_clear_all_pending_data (demux);
|
|
GST_M3U8_CLIENT_UNLOCK (hlsdemux->client);
|
|
}
|
|
|
|
static gchar *
|
|
gst_hls_src_buf_to_utf8_playlist (GstBuffer * buf)
|
|
{
|
|
GstMapInfo info;
|
|
gchar *playlist;
|
|
|
|
if (!gst_buffer_map (buf, &info, GST_MAP_READ))
|
|
goto map_error;
|
|
|
|
if (!g_utf8_validate ((gchar *) info.data, info.size, NULL))
|
|
goto validate_error;
|
|
|
|
/* alloc size + 1 to end with a null character */
|
|
playlist = g_malloc0 (info.size + 1);
|
|
memcpy (playlist, info.data, info.size);
|
|
|
|
gst_buffer_unmap (buf, &info);
|
|
return playlist;
|
|
|
|
validate_error:
|
|
gst_buffer_unmap (buf, &info);
|
|
map_error:
|
|
return NULL;
|
|
}
|
|
|
|
static gint
|
|
gst_hls_demux_find_variant_match (const GstHLSVariantStream * a,
|
|
const GstHLSVariantStream * b)
|
|
{
|
|
if (g_strcmp0 (a->name, b->name) == 0 &&
|
|
a->bandwidth == b->bandwidth &&
|
|
a->program_id == b->program_id &&
|
|
g_strcmp0 (a->codecs, b->codecs) == 0 &&
|
|
a->width == b->width &&
|
|
a->height == b->height && a->iframe == b->iframe) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* Update the master playlist, which contains the list of available
|
|
* variants */
|
|
static gboolean
|
|
gst_hls_demux_update_variant_playlist (GstHLSDemux * hlsdemux, gchar * data,
|
|
const gchar * uri, const gchar * base_uri)
|
|
{
|
|
GstHLSMasterPlaylist *new_master, *old;
|
|
gboolean ret = FALSE;
|
|
GList *l, *unmatched_lists;
|
|
GstHLSVariantStream *new_variant;
|
|
|
|
new_master = gst_hls_master_playlist_new_from_data (data, base_uri ? base_uri : uri); // FIXME: check which uri to use here
|
|
|
|
if (new_master == NULL)
|
|
return ret;
|
|
|
|
if (new_master->is_simple) {
|
|
// FIXME: we should be able to support this though, in the unlikely
|
|
// case that it changed?
|
|
GST_ERROR
|
|
("Cannot update variant playlist: New playlist is not a variant playlist");
|
|
gst_hls_master_playlist_unref (new_master);
|
|
return FALSE;
|
|
}
|
|
|
|
GST_M3U8_CLIENT_LOCK (self);
|
|
|
|
if (hlsdemux->master->is_simple) {
|
|
GST_ERROR
|
|
("Cannot update variant playlist: Current playlist is not a variant playlist");
|
|
goto out;
|
|
}
|
|
|
|
/* Now see if the variant playlist still has the same lists */
|
|
unmatched_lists = g_list_copy (hlsdemux->master->variants);
|
|
for (l = new_master->variants; l != NULL; l = l->next) {
|
|
GList *match = g_list_find_custom (unmatched_lists, l->data,
|
|
(GCompareFunc) gst_hls_demux_find_variant_match);
|
|
|
|
if (match) {
|
|
GstHLSVariantStream *variant = l->data;
|
|
GstHLSVariantStream *old = match->data;
|
|
|
|
unmatched_lists = g_list_delete_link (unmatched_lists, match);
|
|
/* FIXME: Deal with losing position due to missing an update */
|
|
variant->m3u8->sequence_position = old->m3u8->sequence_position;
|
|
variant->m3u8->sequence = old->m3u8->sequence;
|
|
}
|
|
}
|
|
|
|
if (unmatched_lists != NULL) {
|
|
GST_WARNING ("Unable to match all playlists");
|
|
|
|
for (l = unmatched_lists; l != NULL; l = l->next) {
|
|
if (l->data == hlsdemux->current_variant) {
|
|
GST_WARNING ("Unable to match current playlist");
|
|
}
|
|
}
|
|
|
|
g_list_free (unmatched_lists);
|
|
}
|
|
|
|
/* Switch out the variant playlist */
|
|
old = hlsdemux->master;
|
|
|
|
// FIXME: check all this and also switch of variants, if anything needs updating
|
|
hlsdemux->master = new_master;
|
|
|
|
if (hlsdemux->current_variant == NULL) {
|
|
new_variant = new_master->default_variant;
|
|
} else {
|
|
/* Find the same variant in the new playlist */
|
|
new_variant =
|
|
gst_hls_master_playlist_get_matching_variant (new_master,
|
|
hlsdemux->current_variant);
|
|
}
|
|
|
|
/* Use the function to set the current variant, as it copies over data */
|
|
if (new_variant != NULL)
|
|
gst_hls_demux_set_current_variant (hlsdemux, new_variant);
|
|
|
|
gst_hls_master_playlist_unref (old);
|
|
|
|
ret = (hlsdemux->current_variant != NULL);
|
|
out:
|
|
GST_M3U8_CLIENT_UNLOCK (self);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_update_rendition_manifest (GstHLSDemux * demux,
|
|
GstHLSMedia * media, GError ** err)
|
|
{
|
|
GstAdaptiveDemux *adaptive_demux = GST_ADAPTIVE_DEMUX (demux);
|
|
GstFragment *download;
|
|
GstBuffer *buf;
|
|
gchar *playlist;
|
|
const gchar *main_uri;
|
|
GstM3U8 *m3u8;
|
|
gchar *uri = media->uri;
|
|
|
|
main_uri = gst_adaptive_demux_get_manifest_ref_uri (adaptive_demux);
|
|
download =
|
|
gst_uri_downloader_fetch_uri (adaptive_demux->downloader, uri, main_uri,
|
|
TRUE, TRUE, TRUE, err);
|
|
|
|
if (download == NULL)
|
|
return FALSE;
|
|
|
|
m3u8 = media->playlist;
|
|
|
|
/* Set the base URI of the playlist to the redirect target if any */
|
|
if (download->redirect_permanent && download->redirect_uri) {
|
|
gst_m3u8_set_uri (m3u8, download->redirect_uri, NULL, media->name);
|
|
} else {
|
|
gst_m3u8_set_uri (m3u8, download->uri, download->redirect_uri, media->name);
|
|
}
|
|
|
|
buf = gst_fragment_get_buffer (download);
|
|
playlist = gst_hls_src_buf_to_utf8_playlist (buf);
|
|
gst_buffer_unref (buf);
|
|
g_object_unref (download);
|
|
|
|
if (playlist == NULL) {
|
|
GST_WARNING_OBJECT (demux, "Couldn't validate playlist encoding");
|
|
g_set_error (err, GST_STREAM_ERROR, GST_STREAM_ERROR_WRONG_TYPE,
|
|
"Couldn't validate playlist encoding");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!gst_m3u8_update (m3u8, playlist)) {
|
|
GST_WARNING_OBJECT (demux, "Couldn't update playlist");
|
|
g_set_error (err, GST_STREAM_ERROR, GST_STREAM_ERROR_FAILED,
|
|
"Couldn't update playlist");
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_update_playlist (GstHLSDemux * demux, gboolean update,
|
|
GError ** err)
|
|
{
|
|
GstAdaptiveDemux *adaptive_demux = GST_ADAPTIVE_DEMUX (demux);
|
|
GstFragment *download;
|
|
GstBuffer *buf;
|
|
gchar *playlist;
|
|
gboolean main_checked = FALSE;
|
|
const gchar *main_uri;
|
|
GstM3U8 *m3u8;
|
|
gchar *uri;
|
|
gint i;
|
|
|
|
retry:
|
|
uri = gst_m3u8_get_uri (demux->current_variant->m3u8);
|
|
main_uri = gst_adaptive_demux_get_manifest_ref_uri (adaptive_demux);
|
|
download =
|
|
gst_uri_downloader_fetch_uri (adaptive_demux->downloader, uri, main_uri,
|
|
TRUE, TRUE, TRUE, err);
|
|
if (download == NULL) {
|
|
gchar *base_uri;
|
|
|
|
if (!update || main_checked || demux->master->is_simple) {
|
|
g_free (uri);
|
|
return FALSE;
|
|
}
|
|
g_clear_error (err);
|
|
GST_INFO_OBJECT (demux,
|
|
"Updating playlist %s failed, attempt to refresh variant playlist %s",
|
|
uri, main_uri);
|
|
download =
|
|
gst_uri_downloader_fetch_uri (adaptive_demux->downloader,
|
|
main_uri, NULL, TRUE, TRUE, TRUE, err);
|
|
if (download == NULL) {
|
|
g_free (uri);
|
|
return FALSE;
|
|
}
|
|
|
|
buf = gst_fragment_get_buffer (download);
|
|
playlist = gst_hls_src_buf_to_utf8_playlist (buf);
|
|
gst_buffer_unref (buf);
|
|
|
|
if (playlist == NULL) {
|
|
GST_WARNING_OBJECT (demux,
|
|
"Failed to validate variant playlist encoding");
|
|
g_free (uri);
|
|
g_object_unref (download);
|
|
g_set_error (err, GST_STREAM_ERROR, GST_STREAM_ERROR_WRONG_TYPE,
|
|
"Couldn't validate playlist encoding");
|
|
return FALSE;
|
|
}
|
|
|
|
g_free (uri);
|
|
if (download->redirect_permanent && download->redirect_uri) {
|
|
uri = download->redirect_uri;
|
|
base_uri = NULL;
|
|
} else {
|
|
uri = download->uri;
|
|
base_uri = download->redirect_uri;
|
|
}
|
|
|
|
if (!gst_hls_demux_update_variant_playlist (demux, playlist, uri, base_uri)) {
|
|
GST_WARNING_OBJECT (demux, "Failed to update the variant playlist");
|
|
g_object_unref (download);
|
|
g_set_error (err, GST_STREAM_ERROR, GST_STREAM_ERROR_FAILED,
|
|
"Couldn't update playlist");
|
|
return FALSE;
|
|
}
|
|
|
|
g_object_unref (download);
|
|
|
|
main_checked = TRUE;
|
|
goto retry;
|
|
}
|
|
g_free (uri);
|
|
|
|
m3u8 = demux->current_variant->m3u8;
|
|
|
|
/* Set the base URI of the playlist to the redirect target if any */
|
|
if (download->redirect_permanent && download->redirect_uri) {
|
|
gst_m3u8_set_uri (m3u8, download->redirect_uri, NULL,
|
|
demux->current_variant->name);
|
|
} else {
|
|
gst_m3u8_set_uri (m3u8, download->uri, download->redirect_uri,
|
|
demux->current_variant->name);
|
|
}
|
|
|
|
buf = gst_fragment_get_buffer (download);
|
|
playlist = gst_hls_src_buf_to_utf8_playlist (buf);
|
|
gst_buffer_unref (buf);
|
|
g_object_unref (download);
|
|
|
|
if (playlist == NULL) {
|
|
GST_WARNING_OBJECT (demux, "Couldn't validate playlist encoding");
|
|
g_set_error (err, GST_STREAM_ERROR, GST_STREAM_ERROR_WRONG_TYPE,
|
|
"Couldn't validate playlist encoding");
|
|
return FALSE;
|
|
}
|
|
|
|
if (!gst_m3u8_update (m3u8, playlist)) {
|
|
GST_WARNING_OBJECT (demux, "Couldn't update playlist");
|
|
g_set_error (err, GST_STREAM_ERROR, GST_STREAM_ERROR_FAILED,
|
|
"Couldn't update playlist");
|
|
return FALSE;
|
|
}
|
|
|
|
for (i = 0; i < GST_HLS_N_MEDIA_TYPES; ++i) {
|
|
GList *mlist = demux->current_variant->media[i];
|
|
|
|
while (mlist != NULL) {
|
|
GstHLSMedia *media = mlist->data;
|
|
|
|
if (media->uri == NULL) {
|
|
/* No uri means this is a placeholder for a stream
|
|
* contained in another mux */
|
|
mlist = mlist->next;
|
|
continue;
|
|
}
|
|
GST_LOG_OBJECT (demux,
|
|
"Updating playlist for media of type %d - %s, uri: %s", i,
|
|
media->name, media->uri);
|
|
|
|
if (!gst_hls_demux_update_rendition_manifest (demux, media, err))
|
|
return FALSE;
|
|
|
|
mlist = mlist->next;
|
|
}
|
|
}
|
|
|
|
/* If it's a live source, do not let the sequence number go beyond
|
|
* three fragments before the end of the list */
|
|
if (update == FALSE && gst_m3u8_is_live (m3u8)) {
|
|
gint64 last_sequence, first_sequence;
|
|
|
|
GST_M3U8_CLIENT_LOCK (demux->client);
|
|
last_sequence =
|
|
GST_M3U8_MEDIA_FILE (g_list_last (m3u8->files)->data)->sequence;
|
|
first_sequence =
|
|
GST_M3U8_MEDIA_FILE (g_list_first (m3u8->files)->data)->sequence;
|
|
|
|
GST_DEBUG_OBJECT (demux,
|
|
"sequence:%" G_GINT64_FORMAT " , first_sequence:%" G_GINT64_FORMAT
|
|
" , last_sequence:%" G_GINT64_FORMAT, m3u8->sequence,
|
|
first_sequence, last_sequence);
|
|
if (m3u8->sequence > last_sequence - 3) {
|
|
//demux->need_segment = TRUE;
|
|
/* Make sure we never go below the minimum sequence number */
|
|
m3u8->sequence = MAX (first_sequence, last_sequence - 3);
|
|
GST_DEBUG_OBJECT (demux,
|
|
"Sequence is beyond playlist. Moving back to %" G_GINT64_FORMAT,
|
|
m3u8->sequence);
|
|
}
|
|
GST_M3U8_CLIENT_UNLOCK (demux->client);
|
|
} else if (!gst_m3u8_is_live (m3u8)) {
|
|
GstClockTime current_pos, target_pos;
|
|
guint sequence = 0;
|
|
GList *walk;
|
|
|
|
/* Sequence numbers are not guaranteed to be the same in different
|
|
* playlists, so get the correct fragment here based on the current
|
|
* position
|
|
*/
|
|
GST_M3U8_CLIENT_LOCK (demux->client);
|
|
|
|
/* Valid because hlsdemux only has a single output */
|
|
if (GST_ADAPTIVE_DEMUX_CAST (demux)->streams) {
|
|
GstAdaptiveDemuxStream *stream =
|
|
GST_ADAPTIVE_DEMUX_CAST (demux)->streams->data;
|
|
target_pos = stream->segment.position;
|
|
} else {
|
|
target_pos = 0;
|
|
}
|
|
if (GST_CLOCK_TIME_IS_VALID (m3u8->sequence_position)) {
|
|
target_pos = MAX (target_pos, m3u8->sequence_position);
|
|
}
|
|
|
|
GST_LOG_OBJECT (demux, "Looking for sequence position %"
|
|
GST_TIME_FORMAT " in updated playlist", GST_TIME_ARGS (target_pos));
|
|
|
|
current_pos = 0;
|
|
for (walk = m3u8->files; walk; walk = walk->next) {
|
|
GstM3U8MediaFile *file = walk->data;
|
|
|
|
sequence = file->sequence;
|
|
if (current_pos <= target_pos
|
|
&& target_pos < current_pos + file->duration) {
|
|
break;
|
|
}
|
|
current_pos += file->duration;
|
|
}
|
|
/* End of playlist */
|
|
if (!walk)
|
|
sequence++;
|
|
m3u8->sequence = sequence;
|
|
m3u8->sequence_position = current_pos;
|
|
GST_M3U8_CLIENT_UNLOCK (demux->client);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_hls_demux_change_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);
|
|
GstAdaptiveDemuxStream *stream;
|
|
|
|
g_return_val_if_fail (adaptive_demux->streams != NULL, FALSE);
|
|
|
|
stream = adaptive_demux->streams->data;
|
|
|
|
previous_variant = demux->current_variant;
|
|
new_variant =
|
|
gst_hls_master_playlist_get_variant_for_bitrate (demux->master,
|
|
demux->current_variant, max_bitrate);
|
|
|
|
GST_M3U8_CLIENT_LOCK (demux->client);
|
|
|
|
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_M3U8_CLIENT_UNLOCK (demux->client);
|
|
return TRUE;
|
|
}
|
|
|
|
GST_M3U8_CLIENT_UNLOCK (demux->client);
|
|
|
|
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)) {
|
|
const gchar *main_uri;
|
|
gchar *uri;
|
|
|
|
uri = gst_m3u8_get_uri (new_variant->m3u8);
|
|
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)));
|
|
g_free (uri);
|
|
if (changed)
|
|
*changed = TRUE;
|
|
stream->discont = TRUE;
|
|
} else {
|
|
GstHLSVariantStream *failover_variant = NULL;
|
|
GList *failover;
|
|
|
|
GST_INFO_OBJECT (demux, "Unable to update playlist. Switching back");
|
|
GST_M3U8_CLIENT_LOCK (demux->client);
|
|
|
|
/* 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_M3U8_CLIENT_UNLOCK (demux->client);
|
|
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)
|
|
return FALSE;
|
|
} else {
|
|
lowest_variant = demux->master->variants->data;
|
|
if (new_bandwidth == lowest_variant->bandwidth)
|
|
return FALSE;
|
|
}
|
|
return gst_hls_demux_change_playlist (demux, new_bandwidth - 1, changed);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
#if defined(HAVE_OPENSSL)
|
|
static gboolean
|
|
gst_hls_demux_stream_decrypt_start (GstHLSDemuxStream * stream,
|
|
const guint8 * key_data, const guint8 * iv_data)
|
|
{
|
|
EVP_CIPHER_CTX_init (&stream->aes_ctx);
|
|
if (!EVP_DecryptInit_ex (&stream->aes_ctx, EVP_aes_128_cbc (), NULL, key_data,
|
|
iv_data))
|
|
return FALSE;
|
|
EVP_CIPHER_CTX_set_padding (&stream->aes_ctx, 0);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
decrypt_fragment (GstHLSDemuxStream * stream, gsize length,
|
|
const guint8 * encrypted_data, guint8 * decrypted_data)
|
|
{
|
|
int len, flen = 0;
|
|
|
|
if (G_UNLIKELY (length > G_MAXINT || length % 16 != 0))
|
|
return FALSE;
|
|
|
|
len = (int) length;
|
|
if (!EVP_DecryptUpdate (&stream->aes_ctx, decrypted_data, &len,
|
|
encrypted_data, len))
|
|
return FALSE;
|
|
EVP_DecryptFinal_ex (&stream->aes_ctx, decrypted_data + len, &flen);
|
|
g_return_val_if_fail (len + flen == length, FALSE);
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_stream_decrypt_end (GstHLSDemuxStream * stream)
|
|
{
|
|
EVP_CIPHER_CTX_cleanup (&stream->aes_ctx);
|
|
}
|
|
|
|
#elif defined(HAVE_NETTLE)
|
|
static gboolean
|
|
gst_hls_demux_stream_decrypt_start (GstHLSDemuxStream * stream,
|
|
const guint8 * key_data, const guint8 * iv_data)
|
|
{
|
|
aes_set_decrypt_key (&stream->aes_ctx.ctx, 16, key_data);
|
|
CBC_SET_IV (&stream->aes_ctx, iv_data);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
decrypt_fragment (GstHLSDemuxStream * stream, gsize length,
|
|
const guint8 * encrypted_data, guint8 * decrypted_data)
|
|
{
|
|
if (length % 16 != 0)
|
|
return FALSE;
|
|
|
|
CBC_DECRYPT (&stream->aes_ctx, aes_decrypt, length, decrypted_data,
|
|
encrypted_data);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_stream_decrypt_end (GstHLSDemuxStream * stream)
|
|
{
|
|
/* NOP */
|
|
}
|
|
|
|
#else
|
|
static gboolean
|
|
gst_hls_demux_stream_decrypt_start (GstHLSDemuxStream * stream,
|
|
const guint8 * key_data, const guint8 * iv_data)
|
|
{
|
|
gcry_error_t err = 0;
|
|
gboolean ret = FALSE;
|
|
|
|
err =
|
|
gcry_cipher_open (&stream->aes_ctx, GCRY_CIPHER_AES128,
|
|
GCRY_CIPHER_MODE_CBC, 0);
|
|
if (err)
|
|
goto out;
|
|
err = gcry_cipher_setkey (stream->aes_ctx, key_data, 16);
|
|
if (err)
|
|
goto out;
|
|
err = gcry_cipher_setiv (stream->aes_ctx, iv_data, 16);
|
|
if (!err)
|
|
ret = TRUE;
|
|
|
|
out:
|
|
if (!ret)
|
|
if (stream->aes_ctx)
|
|
gcry_cipher_close (stream->aes_ctx);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
decrypt_fragment (GstHLSDemuxStream * stream, gsize length,
|
|
const guint8 * encrypted_data, guint8 * decrypted_data)
|
|
{
|
|
gcry_error_t err = 0;
|
|
|
|
err = gcry_cipher_decrypt (stream->aes_ctx, decrypted_data, length,
|
|
encrypted_data, length);
|
|
|
|
return err == 0;
|
|
}
|
|
|
|
static void
|
|
gst_hls_demux_stream_decrypt_end (GstHLSDemuxStream * stream)
|
|
{
|
|
if (stream->aes_ctx) {
|
|
gcry_cipher_close (stream->aes_ctx);
|
|
stream->aes_ctx = NULL;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static GstBuffer *
|
|
gst_hls_demux_decrypt_fragment (GstHLSDemux * demux, GstHLSDemuxStream * stream,
|
|
GstBuffer * encrypted_buffer, GError ** err)
|
|
{
|
|
GstBuffer *decrypted_buffer = NULL;
|
|
GstMapInfo encrypted_info, decrypted_info;
|
|
|
|
decrypted_buffer =
|
|
gst_buffer_new_allocate (NULL, gst_buffer_get_size (encrypted_buffer),
|
|
NULL);
|
|
|
|
gst_buffer_map (encrypted_buffer, &encrypted_info, GST_MAP_READ);
|
|
gst_buffer_map (decrypted_buffer, &decrypted_info, GST_MAP_WRITE);
|
|
|
|
if (!decrypt_fragment (stream, encrypted_info.size,
|
|
encrypted_info.data, decrypted_info.data))
|
|
goto decrypt_error;
|
|
|
|
|
|
gst_buffer_unmap (decrypted_buffer, &decrypted_info);
|
|
gst_buffer_unmap (encrypted_buffer, &encrypted_info);
|
|
|
|
gst_buffer_unref (encrypted_buffer);
|
|
|
|
return decrypted_buffer;
|
|
|
|
decrypt_error:
|
|
GST_ERROR_OBJECT (demux, "Failed to decrypt fragment");
|
|
g_set_error (err, GST_STREAM_ERROR, GST_STREAM_ERROR_DECRYPT,
|
|
"Failed to decrypt fragment");
|
|
|
|
gst_buffer_unmap (decrypted_buffer, &decrypted_info);
|
|
gst_buffer_unmap (encrypted_buffer, &encrypted_info);
|
|
|
|
gst_buffer_unref (encrypted_buffer);
|
|
gst_buffer_unref (decrypted_buffer);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static gint64
|
|
gst_hls_demux_get_manifest_update_interval (GstAdaptiveDemux * demux)
|
|
{
|
|
GstHLSDemux *hlsdemux = GST_HLS_DEMUX_CAST (demux);
|
|
GstClockTime target_duration;
|
|
|
|
if (hlsdemux->current_variant) {
|
|
target_duration =
|
|
gst_m3u8_get_target_duration (hlsdemux->current_variant->m3u8);
|
|
} else {
|
|
target_duration = 5 * GST_SECOND;
|
|
}
|
|
|
|
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->current_variant) {
|
|
ret =
|
|
gst_m3u8_get_seek_range (hlsdemux->current_variant->m3u8, start, stop);
|
|
}
|
|
|
|
return ret;
|
|
}
|