/* * DASH demux plugin for GStreamer * * gstdashdemux.c * * Copyright (C) 2012 Orange * * Authors: * David Corvoysier * Hamid Zakari * * Copyright (C) 2013 Smart TV Alliance * Author: Thiago Sousa Santos , Collabora Ltd. * * 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.1 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 (COPYING); if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /** * SECTION:element-dashdemux * * DASH demuxer element. * Example launch line * |[ * gst-launch-1.0 playbin uri="http://www-itec.uni-klu.ac.at/ftp/datasets/mmsys12/RedBullPlayStreets/redbull_4s/RedBullPlayStreets_4s_isoffmain_DIS_23009_1_v_2_1c2_2011_08_30.mpd" * ]| */ /* Implementation notes: * * The following section describes how dashdemux works internally. * * Introduction: * * dashdemux is a "fake" demux, as unlike traditional demux elements, it * doesn't split data streams contained in an enveloppe to expose them * to downstream decoding elements. * * Instead, it parses an XML file called a manifest to identify a set of * individual stream fragments it needs to fetch and expose to the actual * demux elements that will handle them (this behavior is sometimes * referred as the "demux after a demux" scenario). * * For a given section of content, several representations corresponding * to different bitrates may be available: dashdemux will select the most * appropriate representation based on local conditions (typically the * available bandwidth and the amount of buffering available, capped by * a maximum allowed bitrate). * * The representation selection algorithm can be configured using * specific properties: max bitrate, min/max buffering, bandwidth ratio. * * * General Design: * * dashdemux has a single sink pad that accepts the data corresponding * to the manifest, typically fetched from an HTTP or file source. * * dashdemux exposes the streams it recreates based on the fragments it * fetches through dedicated src pads corresponding to the caps of the * fragments container (ISOBMFF/MP4 or MPEG2TS). * * During playback, new representations will typically be exposed as a * new set of pads (see 'Switching between representations' below). * * Fragments downloading is performed using a dedicated task that fills * an internal queue. Another task is in charge of popping fragments * from the queue and pushing them downstream. * * Switching between representations: * * Decodebin supports scenarios allowing to seamlessly switch from one * stream to another inside the same "decoding chain". * * To achieve that, it combines the elements it autoplugged in chains * and groups, allowing only one decoding group to be active at a given * time for a given chain. * * A chain can signal decodebin that it is complete by sending a * no-more-pads event, but even after that new pads can be added to * create new subgroups, providing that a new no-more-pads event is sent. * * We take advantage of that to dynamically create a new decoding group * in order to select a different representation during playback. * * Typically, assuming that each fragment contains both audio and video, * the following tree would be created: * * chain "DASH Demux" * |_ group "Representation set 1" * | |_ chain "Qt Demux 0" * | |_ group "Stream 0" * | |_ chain "H264" * | |_ chain "AAC" * |_ group "Representation set 2" * |_ chain "Qt Demux 1" * |_ group "Stream 1" * |_ chain "H264" * |_ chain "AAC" * * Or, if audio and video are contained in separate fragments: * * chain "DASH Demux" * |_ group "Representation set 1" * | |_ chain "Qt Demux 0" * | | |_ group "Stream 0" * | | |_ chain "H264" * | |_ chain "Qt Demux 1" * | |_ group "Stream 1" * | |_ chain "AAC" * |_ group "Representation set 2" * |_ chain "Qt Demux 3" * | |_ group "Stream 2" * | |_ chain "H264" * |_ chain "Qt Demux 4" * |_ group "Stream 3" * |_ chain "AAC" * * In both cases, when switching from Set 1 to Set 2 an EOS is sent on * each end pad corresponding to Rep 0, triggering the "drain" state to * propagate upstream. * Once both EOS have been processed, the "Set 1" group is completely * drained, and decodebin2 will switch to the "Set 2" group. * * Note: nothing can be pushed to the new decoding group before the * old one has been drained, which means that in order to be able to * adapt quickly to bandwidth changes, we will not be able to rely * on downstream buffering, and will instead manage an internal queue. * */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include #include #include "gst/gst-i18n-plugin.h" #include "gstdashdemux.h" #include "gstdash_debug.h" static GstStaticPadTemplate gst_dash_demux_videosrc_template = GST_STATIC_PAD_TEMPLATE ("video_%02u", GST_PAD_SRC, GST_PAD_SOMETIMES, GST_STATIC_CAPS_ANY); static GstStaticPadTemplate gst_dash_demux_audiosrc_template = GST_STATIC_PAD_TEMPLATE ("audio_%02u", GST_PAD_SRC, GST_PAD_SOMETIMES, GST_STATIC_CAPS_ANY); static GstStaticPadTemplate gst_dash_demux_subtitlesrc_template = GST_STATIC_PAD_TEMPLATE ("subtitle_%02u", 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/dash+xml")); GST_DEBUG_CATEGORY (gst_dash_demux_debug); #define GST_CAT_DEFAULT gst_dash_demux_debug enum { PROP_0, PROP_MAX_BUFFERING_TIME, PROP_BANDWIDTH_USAGE, PROP_MAX_BITRATE, PROP_MAX_VIDEO_WIDTH, PROP_MAX_VIDEO_HEIGHT, PROP_MAX_VIDEO_FRAMERATE, PROP_PRESENTATION_DELAY, PROP_LAST }; /* Default values for properties */ #define DEFAULT_MAX_BUFFERING_TIME 30 /* in seconds */ #define DEFAULT_BANDWIDTH_USAGE 0.8 /* 0 to 1 */ #define DEFAULT_MAX_BITRATE 0 /* in bit/s */ #define DEFAULT_MAX_VIDEO_WIDTH 0 #define DEFAULT_MAX_VIDEO_HEIGHT 0 #define DEFAULT_MAX_VIDEO_FRAMERATE_N 0 #define DEFAULT_MAX_VIDEO_FRAMERATE_D 1 #define DEFAULT_PRESENTATION_DELAY NULL /* zero */ /* Clock drift compensation for live streams */ #define SLOW_CLOCK_UPDATE_INTERVAL (1000000 * 30 * 60) /* 30 minutes */ #define FAST_CLOCK_UPDATE_INTERVAL (1000000 * 30) /* 30 seconds */ #define SUPPORTED_CLOCK_FORMATS (GST_MPD_UTCTIMING_TYPE_NTP | GST_MPD_UTCTIMING_TYPE_HTTP_HEAD | GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE | GST_MPD_UTCTIMING_TYPE_HTTP_ISO | GST_MPD_UTCTIMING_TYPE_HTTP_NTP) #define NTP_TO_UNIX_EPOCH G_GUINT64_CONSTANT(2208988800) /* difference (in seconds) between NTP epoch and Unix epoch */ struct _GstDashDemuxClockDrift { GMutex clock_lock; /* used to protect access to struct */ guint selected_url; gint64 next_update; /* @clock_compensation: amount (in usecs) to add to client's idea of now to map it to the server's idea of now */ GTimeSpan clock_compensation; GstClock *ntp_clock; }; typedef struct { guint64 start_offset, end_offset; /* TODO: Timestamp and duration */ } GstDashStreamSyncSample; /* GObject */ static void gst_dash_demux_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); static void gst_dash_demux_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); static void gst_dash_demux_dispose (GObject * obj); /* GstAdaptiveDemux */ static GstClockTime gst_dash_demux_get_duration (GstAdaptiveDemux * ademux); static gboolean gst_dash_demux_is_live (GstAdaptiveDemux * ademux); static void gst_dash_demux_reset (GstAdaptiveDemux * ademux); static gboolean gst_dash_demux_process_manifest (GstAdaptiveDemux * ademux, GstBuffer * buf); static gboolean gst_dash_demux_seek (GstAdaptiveDemux * demux, GstEvent * seek); static GstFlowReturn gst_dash_demux_stream_update_fragment_info (GstAdaptiveDemuxStream * stream); static GstFlowReturn gst_dash_demux_stream_seek (GstAdaptiveDemuxStream * stream, gboolean forward, GstSeekFlags flags, GstClockTime ts, GstClockTime * final_ts); static gboolean gst_dash_demux_stream_has_next_fragment (GstAdaptiveDemuxStream * stream); static GstFlowReturn gst_dash_demux_stream_advance_fragment (GstAdaptiveDemuxStream * stream); static gboolean gst_dash_demux_stream_advance_subfragment (GstAdaptiveDemuxStream * stream); static gboolean gst_dash_demux_stream_select_bitrate (GstAdaptiveDemuxStream * stream, guint64 bitrate); static gint64 gst_dash_demux_get_manifest_update_interval (GstAdaptiveDemux * demux); static GstFlowReturn gst_dash_demux_update_manifest_data (GstAdaptiveDemux * demux, GstBuffer * buf); static gint64 gst_dash_demux_stream_get_fragment_waiting_time (GstAdaptiveDemuxStream * stream); static void gst_dash_demux_advance_period (GstAdaptiveDemux * demux); static gboolean gst_dash_demux_has_next_period (GstAdaptiveDemux * demux); static GstFlowReturn gst_dash_demux_data_received (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream, GstBuffer * buffer); static gboolean gst_dash_demux_stream_fragment_start (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream); static GstFlowReturn gst_dash_demux_stream_fragment_finished (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream); static gboolean gst_dash_demux_need_another_chunk (GstAdaptiveDemuxStream * stream); /* GstDashDemux */ static gboolean gst_dash_demux_setup_all_streams (GstDashDemux * demux); static void gst_dash_demux_stream_free (GstAdaptiveDemuxStream * stream); static GstCaps *gst_dash_demux_get_input_caps (GstDashDemux * demux, GstActiveStream * stream); static GstPad *gst_dash_demux_create_pad (GstDashDemux * demux, GstActiveStream * stream); static GstDashDemuxClockDrift *gst_dash_demux_clock_drift_new (GstDashDemux * demux); static void gst_dash_demux_clock_drift_free (GstDashDemuxClockDrift *); static gboolean gst_dash_demux_poll_clock_drift (GstDashDemux * demux); static GTimeSpan gst_dash_demux_get_clock_compensation (GstDashDemux * demux); static GDateTime *gst_dash_demux_get_server_now_utc (GstDashDemux * demux); #define SIDX(s) (&(s)->sidx_parser.sidx) #define SIDX_ENTRY(s,i) (&(SIDX(s)->entries[(i)])) #define SIDX_CURRENT_ENTRY(s) SIDX_ENTRY(s, SIDX(s)->entry_index) static void gst_dash_demux_send_content_protection_event (gpointer cp_data, gpointer stream); #define gst_dash_demux_parent_class parent_class G_DEFINE_TYPE_WITH_CODE (GstDashDemux, gst_dash_demux, GST_TYPE_ADAPTIVE_DEMUX, GST_DEBUG_CATEGORY_INIT (gst_dash_demux_debug, "dashdemux", 0, "dashdemux element") ); static void gst_dash_demux_dispose (GObject * obj) { GstDashDemux *demux = GST_DASH_DEMUX (obj); gst_dash_demux_reset (GST_ADAPTIVE_DEMUX_CAST (demux)); if (demux->client) { gst_mpd_client_free (demux->client); demux->client = NULL; } g_mutex_clear (&demux->client_lock); gst_dash_demux_clock_drift_free (demux->clock_drift); demux->clock_drift = NULL; g_free (demux->default_presentation_delay); G_OBJECT_CLASS (parent_class)->dispose (obj); } static gboolean gst_dash_demux_get_live_seek_range (GstAdaptiveDemux * demux, gint64 * start, gint64 * stop) { GstDashDemux *self = GST_DASH_DEMUX (demux); GDateTime *now; GDateTime *mstart; GTimeSpan stream_now; GstClockTime seg_duration; if (self->client->mpd_node->availabilityStartTime == NULL) return FALSE; seg_duration = gst_mpd_client_get_maximum_segment_duration (self->client); now = gst_dash_demux_get_server_now_utc (self); mstart = gst_date_time_to_g_date_time (self->client-> mpd_node->availabilityStartTime); stream_now = g_date_time_difference (now, mstart); g_date_time_unref (now); g_date_time_unref (mstart); if (stream_now <= 0) return FALSE; *stop = stream_now * GST_USECOND; if (self->client->mpd_node->timeShiftBufferDepth == GST_MPD_DURATION_NONE) { *start = 0; } else { *start = *stop - (self->client->mpd_node->timeShiftBufferDepth * GST_MSECOND); if (*start < 0) *start = 0; } /* As defined in 5.3.9.5.3 of the DASH specification, a segment does not become available until the sum of: * the value of the MPD@availabilityStartTime, * the PeriodStart time of the containing Period * the MPD start time of the Media Segment, and * the MPD duration of the Media Segment. Therefore we need to subtract the media segment duration from the stop time. */ *stop -= seg_duration; return TRUE; } static GstClockTime gst_dash_demux_get_presentation_offset (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); return gst_mpd_parser_get_stream_presentation_offset (dashdemux->client, dashstream->index); } static GstClockTime gst_dash_demux_get_period_start_time (GstAdaptiveDemux * demux) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); return gst_mpd_parser_get_period_start_time (dashdemux->client); } static void gst_dash_demux_class_init (GstDashDemuxClass * klass) { GObjectClass *gobject_class; GstElementClass *gstelement_class; GstAdaptiveDemuxClass *gstadaptivedemux_class; gobject_class = (GObjectClass *) klass; gstelement_class = (GstElementClass *) klass; gstadaptivedemux_class = (GstAdaptiveDemuxClass *) klass; gobject_class->set_property = gst_dash_demux_set_property; gobject_class->get_property = gst_dash_demux_get_property; gobject_class->dispose = gst_dash_demux_dispose; #ifndef GST_REMOVE_DEPRECATED g_object_class_install_property (gobject_class, PROP_MAX_BUFFERING_TIME, g_param_spec_uint ("max-buffering-time", "Maximum buffering time", "Maximum number of seconds of buffer accumulated during playback" "(deprecated)", 2, G_MAXUINT, DEFAULT_MAX_BUFFERING_TIME, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_DEPRECATED)); g_object_class_install_property (gobject_class, PROP_BANDWIDTH_USAGE, g_param_spec_float ("bandwidth-usage", "Bandwidth usage [0..1]", "Percentage of the available bandwidth to use when " "selecting representations (deprecated)", 0, 1, DEFAULT_BANDWIDTH_USAGE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_DEPRECATED)); #endif g_object_class_install_property (gobject_class, PROP_MAX_BITRATE, g_param_spec_uint ("max-bitrate", "Max bitrate", "Max of bitrate supported by target video decoder (0 = no maximum)", 0, G_MAXUINT, DEFAULT_MAX_BITRATE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MAX_VIDEO_WIDTH, g_param_spec_uint ("max-video-width", "Max video width", "Max video width to select (0 = no maximum)", 0, G_MAXUINT, DEFAULT_MAX_VIDEO_WIDTH, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MAX_VIDEO_HEIGHT, g_param_spec_uint ("max-video-height", "Max video height", "Max video height to select (0 = no maximum)", 0, G_MAXUINT, DEFAULT_MAX_VIDEO_HEIGHT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MAX_VIDEO_FRAMERATE, gst_param_spec_fraction ("max-video-framerate", "Max video framerate", "Max video framerate to select (0/1 = no maximum)", 0, 1, G_MAXINT, 1, DEFAULT_MAX_VIDEO_FRAMERATE_N, DEFAULT_MAX_VIDEO_FRAMERATE_D, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_PRESENTATION_DELAY, g_param_spec_string ("presentation-delay", "Presentation delay", "Default presentation delay (in seconds, milliseconds or fragments) (e.g. 12s, 2500ms, 3f)", DEFAULT_PRESENTATION_DELAY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); gst_element_class_add_static_pad_template (gstelement_class, &gst_dash_demux_audiosrc_template); gst_element_class_add_static_pad_template (gstelement_class, &gst_dash_demux_videosrc_template); gst_element_class_add_static_pad_template (gstelement_class, &gst_dash_demux_subtitlesrc_template); gst_element_class_add_static_pad_template (gstelement_class, &sinktemplate); gst_element_class_set_static_metadata (gstelement_class, "DASH Demuxer", "Codec/Demuxer/Adaptive", "Dynamic Adaptive Streaming over HTTP demuxer", "David Corvoysier \n\ Hamid Zakari \n\ Gianluca Gennari "); gstadaptivedemux_class->get_duration = gst_dash_demux_get_duration; gstadaptivedemux_class->is_live = gst_dash_demux_is_live; gstadaptivedemux_class->reset = gst_dash_demux_reset; gstadaptivedemux_class->seek = gst_dash_demux_seek; gstadaptivedemux_class->process_manifest = gst_dash_demux_process_manifest; gstadaptivedemux_class->update_manifest_data = gst_dash_demux_update_manifest_data; gstadaptivedemux_class->get_manifest_update_interval = gst_dash_demux_get_manifest_update_interval; gstadaptivedemux_class->has_next_period = gst_dash_demux_has_next_period; gstadaptivedemux_class->advance_period = gst_dash_demux_advance_period; gstadaptivedemux_class->stream_has_next_fragment = gst_dash_demux_stream_has_next_fragment; gstadaptivedemux_class->stream_advance_fragment = gst_dash_demux_stream_advance_fragment; gstadaptivedemux_class->stream_get_fragment_waiting_time = gst_dash_demux_stream_get_fragment_waiting_time; gstadaptivedemux_class->stream_seek = gst_dash_demux_stream_seek; gstadaptivedemux_class->stream_select_bitrate = gst_dash_demux_stream_select_bitrate; gstadaptivedemux_class->stream_update_fragment_info = gst_dash_demux_stream_update_fragment_info; gstadaptivedemux_class->stream_free = gst_dash_demux_stream_free; gstadaptivedemux_class->get_live_seek_range = gst_dash_demux_get_live_seek_range; gstadaptivedemux_class->get_presentation_offset = gst_dash_demux_get_presentation_offset; gstadaptivedemux_class->get_period_start_time = gst_dash_demux_get_period_start_time; gstadaptivedemux_class->start_fragment = gst_dash_demux_stream_fragment_start; gstadaptivedemux_class->finish_fragment = gst_dash_demux_stream_fragment_finished; gstadaptivedemux_class->data_received = gst_dash_demux_data_received; gstadaptivedemux_class->need_another_chunk = gst_dash_demux_need_another_chunk; } static void gst_dash_demux_init (GstDashDemux * demux) { /* Properties */ demux->max_buffering_time = DEFAULT_MAX_BUFFERING_TIME * GST_SECOND; demux->max_bitrate = DEFAULT_MAX_BITRATE; demux->max_video_width = DEFAULT_MAX_VIDEO_WIDTH; demux->max_video_height = DEFAULT_MAX_VIDEO_HEIGHT; demux->max_video_framerate_n = DEFAULT_MAX_VIDEO_FRAMERATE_N; demux->max_video_framerate_d = DEFAULT_MAX_VIDEO_FRAMERATE_D; demux->default_presentation_delay = DEFAULT_PRESENTATION_DELAY; g_mutex_init (&demux->client_lock); gst_adaptive_demux_set_stream_struct_size (GST_ADAPTIVE_DEMUX_CAST (demux), sizeof (GstDashDemuxStream)); } static void gst_dash_demux_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstAdaptiveDemux *adaptivedemux = GST_ADAPTIVE_DEMUX_CAST (object); GstDashDemux *demux = GST_DASH_DEMUX (object); switch (prop_id) { case PROP_MAX_BUFFERING_TIME: demux->max_buffering_time = g_value_get_uint (value) * GST_SECOND; break; case PROP_BANDWIDTH_USAGE: adaptivedemux->bitrate_limit = g_value_get_float (value); break; case PROP_MAX_BITRATE: demux->max_bitrate = g_value_get_uint (value); break; case PROP_MAX_VIDEO_WIDTH: demux->max_video_width = g_value_get_uint (value); break; case PROP_MAX_VIDEO_HEIGHT: demux->max_video_height = g_value_get_uint (value); break; case PROP_MAX_VIDEO_FRAMERATE: demux->max_video_framerate_n = gst_value_get_fraction_numerator (value); demux->max_video_framerate_d = gst_value_get_fraction_denominator (value); break; case PROP_PRESENTATION_DELAY: g_free (demux->default_presentation_delay); demux->default_presentation_delay = g_value_dup_string (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_dash_demux_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstAdaptiveDemux *adaptivedemux = GST_ADAPTIVE_DEMUX_CAST (object); GstDashDemux *demux = GST_DASH_DEMUX (object); switch (prop_id) { case PROP_MAX_BUFFERING_TIME: g_value_set_uint (value, demux->max_buffering_time / GST_SECOND); break; case PROP_BANDWIDTH_USAGE: g_value_set_float (value, adaptivedemux->bitrate_limit); break; case PROP_MAX_BITRATE: g_value_set_uint (value, demux->max_bitrate); break; case PROP_MAX_VIDEO_WIDTH: g_value_set_uint (value, demux->max_video_width); break; case PROP_MAX_VIDEO_HEIGHT: g_value_set_uint (value, demux->max_video_height); break; case PROP_MAX_VIDEO_FRAMERATE: gst_value_set_fraction (value, demux->max_video_framerate_n, demux->max_video_framerate_d); break; case PROP_PRESENTATION_DELAY: if (demux->default_presentation_delay == NULL) g_value_set_static_string (value, ""); else g_value_set_string (value, demux->default_presentation_delay); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static gboolean gst_dash_demux_setup_mpdparser_streams (GstDashDemux * demux, GstMpdClient * client) { gboolean has_streams = FALSE; GList *adapt_sets, *iter; adapt_sets = gst_mpd_client_get_adaptation_sets (client); for (iter = adapt_sets; iter; iter = g_list_next (iter)) { GstAdaptationSetNode *adapt_set_node = iter->data; gst_mpd_client_setup_streaming (client, adapt_set_node); has_streams = TRUE; } if (!has_streams) { GST_ELEMENT_ERROR (demux, STREAM, DEMUX, ("Manifest has no playable " "streams"), ("No streams could be activated from the manifest")); } return has_streams; } static gboolean gst_dash_demux_setup_all_streams (GstDashDemux * demux) { guint i; GST_DEBUG_OBJECT (demux, "Setting up streams for period %d", gst_mpd_client_get_period_index (demux->client)); /* clean old active stream list, if any */ gst_active_streams_free (demux->client); if (!gst_dash_demux_setup_mpdparser_streams (demux, demux->client)) { return FALSE; } GST_DEBUG_OBJECT (demux, "Creating stream objects"); for (i = 0; i < gst_mpdparser_get_nb_active_stream (demux->client); i++) { GstDashDemuxStream *stream; GstActiveStream *active_stream; GstCaps *caps; GstStructure *s; GstPad *srcpad; gchar *lang = NULL; GstTagList *tags = NULL; active_stream = gst_mpdparser_get_active_stream_by_index (demux->client, i); if (active_stream == NULL) continue; if (demux->trickmode_no_audio && active_stream->mimeType == GST_STREAM_AUDIO) { GST_DEBUG_OBJECT (demux, "Skipping audio stream %d because of TRICKMODE_NO_AUDIO flag", i); continue; } srcpad = gst_dash_demux_create_pad (demux, active_stream); if (srcpad == NULL) continue; caps = gst_dash_demux_get_input_caps (demux, active_stream); GST_LOG_OBJECT (demux, "Creating stream %d %" GST_PTR_FORMAT, i, caps); if (active_stream->cur_adapt_set) { GstAdaptationSetNode *adp_set = active_stream->cur_adapt_set; lang = adp_set->lang; /* Fallback to the language in ContentComponent node */ if (lang == NULL) { GList *it; for (it = adp_set->ContentComponents; it; it = it->next) { GstContentComponentNode *cc_node = it->data; if (cc_node->lang) { lang = cc_node->lang; break; } } } } if (lang) { if (gst_tag_check_language_code (lang)) tags = gst_tag_list_new (GST_TAG_LANGUAGE_CODE, lang, NULL); else tags = gst_tag_list_new (GST_TAG_LANGUAGE_NAME, lang, NULL); } stream = (GstDashDemuxStream *) gst_adaptive_demux_stream_new (GST_ADAPTIVE_DEMUX_CAST (demux), srcpad); stream->active_stream = active_stream; s = gst_caps_get_structure (caps, 0); stream->is_isobmff = gst_structure_has_name (s, "video/quicktime"); stream->first_sync_sample_always_after_moof = TRUE; if (stream->is_isobmff) stream->isobmff_adapter = gst_adapter_new (); gst_adaptive_demux_stream_set_caps (GST_ADAPTIVE_DEMUX_STREAM_CAST (stream), caps); if (tags) gst_adaptive_demux_stream_set_tags (GST_ADAPTIVE_DEMUX_STREAM_CAST (stream), tags); stream->index = i; stream->pending_seek_ts = GST_CLOCK_TIME_NONE; if (active_stream->cur_adapt_set && active_stream->cur_adapt_set->RepresentationBase && active_stream->cur_adapt_set->RepresentationBase->ContentProtection) { GST_DEBUG_OBJECT (demux, "Adding ContentProtection events to source pad"); g_list_foreach (active_stream->cur_adapt_set->RepresentationBase-> ContentProtection, gst_dash_demux_send_content_protection_event, stream); } gst_isoff_sidx_parser_init (&stream->sidx_parser); if (gst_mpd_client_has_isoff_ondemand_profile (demux->client)) stream->sidx_adapter = gst_adapter_new (); } return TRUE; } static void gst_dash_demux_send_content_protection_event (gpointer data, gpointer userdata) { GstDescriptorType *cp = (GstDescriptorType *) data; GstDashDemuxStream *stream = (GstDashDemuxStream *) userdata; GstEvent *event; GstBuffer *pssi; glong pssi_len; gchar *schemeIdUri; if (cp->schemeIdUri == NULL) return; GST_TRACE_OBJECT (stream, "check schemeIdUri %s", cp->schemeIdUri); /* RFC 2141 states: The leading "urn:" sequence is case-insensitive */ schemeIdUri = g_ascii_strdown (cp->schemeIdUri, -1); if (g_str_has_prefix (schemeIdUri, "urn:uuid:")) { pssi_len = strlen (cp->value); pssi = gst_buffer_new_wrapped (g_memdup (cp->value, pssi_len), pssi_len); GST_LOG_OBJECT (stream, "Queuing Protection event on source pad"); /* RFC 4122 states that the hex part of a UUID is in lower case, * but some streams seem to ignore this and use upper case for the * protection system ID */ event = gst_event_new_protection (cp->schemeIdUri + 9, pssi, "dash/mpd"); gst_adaptive_demux_stream_queue_event ((GstAdaptiveDemuxStream *) stream, event); gst_buffer_unref (pssi); } g_free (schemeIdUri); } static GstClockTime gst_dash_demux_get_duration (GstAdaptiveDemux * ademux) { GstDashDemux *demux = GST_DASH_DEMUX_CAST (ademux); g_return_val_if_fail (demux->client != NULL, GST_CLOCK_TIME_NONE); return gst_mpd_client_get_media_presentation_duration (demux->client); } static gboolean gst_dash_demux_is_live (GstAdaptiveDemux * ademux) { GstDashDemux *demux = GST_DASH_DEMUX_CAST (ademux); g_return_val_if_fail (demux->client != NULL, FALSE); return gst_mpd_client_is_live (demux->client); } static gboolean gst_dash_demux_setup_streams (GstAdaptiveDemux * demux) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); gboolean ret = TRUE; GstDateTime *now = NULL; guint period_idx; /* setup video, audio and subtitle streams, starting from first Period if * non-live */ period_idx = 0; if (gst_mpd_client_is_live (dashdemux->client)) { GDateTime *g_now; if (dashdemux->client->mpd_node->availabilityStartTime == NULL) { ret = FALSE; GST_ERROR_OBJECT (demux, "MPD does not have availabilityStartTime"); goto done; } if (dashdemux->clock_drift == NULL) { gchar **urls; urls = gst_mpd_client_get_utc_timing_sources (dashdemux->client, SUPPORTED_CLOCK_FORMATS, NULL); if (urls) { GST_DEBUG_OBJECT (dashdemux, "Found a supported UTCTiming element"); dashdemux->clock_drift = gst_dash_demux_clock_drift_new (dashdemux); gst_dash_demux_poll_clock_drift (dashdemux); } } /* get period index for period encompassing the current time */ g_now = gst_dash_demux_get_server_now_utc (dashdemux); now = gst_date_time_new_from_g_date_time (g_now); if (dashdemux->client->mpd_node->suggestedPresentationDelay != -1) { GstDateTime *target = gst_mpd_client_add_time_difference (now, dashdemux->client->mpd_node->suggestedPresentationDelay * -1000); gst_date_time_unref (now); now = target; } else if (dashdemux->default_presentation_delay) { gint64 dfp = gst_mpd_client_parse_default_presentation_delay (dashdemux->client, dashdemux->default_presentation_delay); GstDateTime *target = gst_mpd_client_add_time_difference (now, dfp); gst_date_time_unref (now); now = target; } period_idx = gst_mpd_client_get_period_index_at_time (dashdemux->client, now); if (period_idx == G_MAXUINT) { #ifndef GST_DISABLE_GST_DEBUG gchar *date_str = gst_date_time_to_iso8601_string (now); GST_DEBUG_OBJECT (demux, "Unable to find live period active at %s", date_str); g_free (date_str); #endif ret = FALSE; goto done; } } if (!gst_mpd_client_set_period_index (dashdemux->client, period_idx) || !gst_dash_demux_setup_all_streams (dashdemux)) { ret = FALSE; goto done; } /* If stream is live, try to find the segment that * is closest to current time */ if (gst_mpd_client_is_live (dashdemux->client)) { GDateTime *gnow; GST_DEBUG_OBJECT (demux, "Seeking to current time of day for live stream "); gnow = gst_date_time_to_g_date_time (now); gst_mpd_client_seek_to_time (dashdemux->client, gnow); g_date_time_unref (gnow); } else { GST_DEBUG_OBJECT (demux, "Seeking to first segment for on-demand stream "); /* start playing from the first segment */ gst_mpd_client_seek_to_first_segment (dashdemux->client); } done: if (now != NULL) gst_date_time_unref (now); return ret; } static gboolean gst_dash_demux_process_manifest (GstAdaptiveDemux * demux, GstBuffer * buf) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); gboolean ret = FALSE; gchar *manifest; GstMapInfo mapinfo; if (dashdemux->client) gst_mpd_client_free (dashdemux->client); dashdemux->client = gst_mpd_client_new (); gst_mpd_client_set_uri_downloader (dashdemux->client, demux->downloader); dashdemux->client->mpd_uri = g_strdup (demux->manifest_uri); dashdemux->client->mpd_base_uri = g_strdup (demux->manifest_base_uri); GST_DEBUG_OBJECT (demux, "Fetched MPD file at URI: %s (base: %s)", dashdemux->client->mpd_uri, GST_STR_NULL (dashdemux->client->mpd_base_uri)); if (gst_buffer_map (buf, &mapinfo, GST_MAP_READ)) { manifest = (gchar *) mapinfo.data; if (gst_mpd_parse (dashdemux->client, manifest, mapinfo.size)) { if (gst_mpd_client_setup_media_presentation (dashdemux->client, 0, 0, NULL)) { ret = TRUE; } else { GST_ELEMENT_ERROR (demux, STREAM, DECODE, ("Incompatible manifest file."), (NULL)); } } gst_buffer_unmap (buf, &mapinfo); } else { GST_WARNING_OBJECT (demux, "Failed to map manifest buffer"); } if (ret) ret = gst_dash_demux_setup_streams (demux); return ret; } static GstPad * gst_dash_demux_create_pad (GstDashDemux * demux, GstActiveStream * stream) { GstPad *pad; GstPadTemplate *tmpl; gchar *name; switch (stream->mimeType) { case GST_STREAM_AUDIO: name = g_strdup_printf ("audio_%02u", demux->n_audio_streams++); tmpl = gst_static_pad_template_get (&gst_dash_demux_audiosrc_template); break; case GST_STREAM_VIDEO: name = g_strdup_printf ("video_%02u", demux->n_video_streams++); tmpl = gst_static_pad_template_get (&gst_dash_demux_videosrc_template); break; case GST_STREAM_APPLICATION: if (gst_mpd_client_active_stream_contains_subtitles (stream)) { name = g_strdup_printf ("subtitle_%02u", demux->n_subtitle_streams++); tmpl = gst_static_pad_template_get (&gst_dash_demux_subtitlesrc_template); } else { return NULL; } break; default: g_assert_not_reached (); return NULL; } /* Create and activate new pads */ pad = gst_pad_new_from_template (tmpl, name); g_free (name); gst_object_unref (tmpl); gst_pad_set_active (pad, TRUE); GST_INFO_OBJECT (demux, "Creating srcpad %s:%s", GST_DEBUG_PAD_NAME (pad)); return pad; } static void gst_dash_demux_reset (GstAdaptiveDemux * ademux) { GstDashDemux *demux = GST_DASH_DEMUX_CAST (ademux); GST_DEBUG_OBJECT (demux, "Resetting demux"); demux->end_of_period = FALSE; demux->end_of_manifest = FALSE; if (demux->client) { gst_mpd_client_free (demux->client); demux->client = NULL; } gst_dash_demux_clock_drift_free (demux->clock_drift); demux->clock_drift = NULL; demux->client = gst_mpd_client_new (); gst_mpd_client_set_uri_downloader (demux->client, ademux->downloader); demux->n_audio_streams = 0; demux->n_video_streams = 0; demux->n_subtitle_streams = 0; demux->trickmode_no_audio = FALSE; demux->allow_trickmode_key_units = TRUE; } static GstCaps * gst_dash_demux_get_video_input_caps (GstDashDemux * demux, GstActiveStream * stream) { guint width = 0, height = 0; gint fps_num = 0, fps_den = 1; gboolean have_fps = FALSE; GstCaps *caps = NULL; if (stream == NULL) return NULL; /* if bitstreamSwitching is true we dont need to swich pads on resolution change */ if (!gst_mpd_client_get_bitstream_switching_flag (stream)) { width = gst_mpd_client_get_video_stream_width (stream); height = gst_mpd_client_get_video_stream_height (stream); have_fps = gst_mpd_client_get_video_stream_framerate (stream, &fps_num, &fps_den); } caps = gst_mpd_client_get_stream_caps (stream); if (caps == NULL) return NULL; if (width > 0 && height > 0) { gst_caps_set_simple (caps, "width", G_TYPE_INT, width, "height", G_TYPE_INT, height, NULL); } if (have_fps) { gst_caps_set_simple (caps, "framerate", GST_TYPE_FRACTION, fps_num, fps_den, NULL); } return caps; } static GstCaps * gst_dash_demux_get_audio_input_caps (GstDashDemux * demux, GstActiveStream * stream) { guint rate = 0, channels = 0; GstCaps *caps = NULL; if (stream == NULL) return NULL; /* if bitstreamSwitching is true we dont need to swich pads on rate/channels change */ if (!gst_mpd_client_get_bitstream_switching_flag (stream)) { channels = gst_mpd_client_get_audio_stream_num_channels (stream); rate = gst_mpd_client_get_audio_stream_rate (stream); } caps = gst_mpd_client_get_stream_caps (stream); if (caps == NULL) return NULL; if (rate > 0) { gst_caps_set_simple (caps, "rate", G_TYPE_INT, rate, NULL); } if (channels > 0) { gst_caps_set_simple (caps, "channels", G_TYPE_INT, channels, NULL); } return caps; } static GstCaps * gst_dash_demux_get_application_input_caps (GstDashDemux * demux, GstActiveStream * stream) { GstCaps *caps = NULL; if (stream == NULL) return NULL; caps = gst_mpd_client_get_stream_caps (stream); if (caps == NULL) return NULL; return caps; } static GstCaps * gst_dash_demux_get_input_caps (GstDashDemux * demux, GstActiveStream * stream) { switch (stream->mimeType) { case GST_STREAM_VIDEO: return gst_dash_demux_get_video_input_caps (demux, stream); case GST_STREAM_AUDIO: return gst_dash_demux_get_audio_input_caps (demux, stream); case GST_STREAM_APPLICATION: return gst_dash_demux_get_application_input_caps (demux, stream); default: return GST_CAPS_NONE; } } static void gst_dash_demux_stream_update_headers_info (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (stream->demux); gchar *path = NULL; gst_mpd_client_get_next_header (dashdemux->client, &path, dashstream->index, &stream->fragment.header_range_start, &stream->fragment.header_range_end); if (path != NULL) { stream->fragment.header_uri = gst_uri_join_strings (gst_mpdparser_get_baseURL (dashdemux->client, dashstream->index), path); g_free (path); path = NULL; } gst_mpd_client_get_next_header_index (dashdemux->client, &path, dashstream->index, &stream->fragment.index_range_start, &stream->fragment.index_range_end); if (path != NULL) { stream->fragment.index_uri = gst_uri_join_strings (gst_mpdparser_get_baseURL (dashdemux->client, dashstream->index), path); g_free (path); } } static GstFlowReturn gst_dash_demux_stream_update_fragment_info (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (stream->demux); GstClockTime ts; GstMediaFragmentInfo fragment; gboolean isombff; gst_adaptive_demux_stream_fragment_clear (&stream->fragment); isombff = gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client); /* Reset chunk size if any */ stream->fragment.chunk_size = 0; if (GST_ADAPTIVE_DEMUX_STREAM_NEED_HEADER (stream) && isombff) { gst_dash_demux_stream_update_headers_info (stream); dashstream->sidx_base_offset = stream->fragment.index_range_end + 1; if (dashstream->sidx_index != 0) { /* request only the index to be downloaded as we need to reposition the * stream to a subsegment */ return GST_FLOW_OK; } } if (dashstream->moof_sync_samples && GST_ADAPTIVE_DEMUX (dashdemux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) { GstDashStreamSyncSample *sync_sample = &g_array_index (dashstream->moof_sync_samples, GstDashStreamSyncSample, dashstream->current_sync_sample); gst_mpd_client_get_next_fragment (dashdemux->client, dashstream->index, &fragment); stream->fragment.uri = fragment.uri; stream->fragment.timestamp = GST_CLOCK_TIME_NONE; stream->fragment.duration = GST_CLOCK_TIME_NONE; stream->fragment.range_start = sync_sample->start_offset; stream->fragment.range_end = sync_sample->end_offset; return GST_FLOW_OK; } if (gst_mpd_client_get_next_fragment_timestamp (dashdemux->client, dashstream->index, &ts)) { if (GST_ADAPTIVE_DEMUX_STREAM_NEED_HEADER (stream)) { gst_adaptive_demux_stream_fragment_clear (&stream->fragment); gst_dash_demux_stream_update_headers_info (stream); } gst_mpd_client_get_next_fragment (dashdemux->client, dashstream->index, &fragment); stream->fragment.uri = fragment.uri; if (isombff && dashstream->sidx_index != 0) { GstSidxBoxEntry *entry = SIDX_CURRENT_ENTRY (dashstream); stream->fragment.range_start = dashstream->sidx_base_offset + entry->offset; stream->fragment.timestamp = entry->pts; stream->fragment.duration = entry->duration; if (stream->demux->segment.rate < 0.0) { stream->fragment.range_end = stream->fragment.range_start + entry->size - 1; } else { stream->fragment.range_end = fragment.range_end; } } else { stream->fragment.timestamp = fragment.timestamp; stream->fragment.duration = fragment.duration; stream->fragment.range_start = MAX (fragment.range_start, dashstream->sidx_base_offset); stream->fragment.range_end = fragment.range_end; } return GST_FLOW_OK; } return GST_FLOW_EOS; } static gint gst_dash_demux_index_entry_search (GstSidxBoxEntry * entry, GstClockTime * ts, gpointer user_data) { GstClockTime entry_ts = entry->pts + entry->duration; if (entry_ts < *ts) return -1; else if (entry->pts > *ts) return 1; else return 0; } static void gst_dash_demux_stream_sidx_seek (GstDashDemuxStream * dashstream, gboolean forward, GstSeekFlags flags, GstClockTime ts, GstClockTime * final_ts) { GstSidxBox *sidx = SIDX (dashstream); GstSidxBoxEntry *entry; gint idx = sidx->entries_count; /* check whether ts is already past the last element or not */ if (sidx->entries[idx - 1].pts + sidx->entries[idx - 1].duration >= ts) { GstSearchMode mode = GST_SEARCH_MODE_BEFORE; if ((flags & GST_SEEK_FLAG_SNAP_NEAREST) == GST_SEEK_FLAG_SNAP_NEAREST) { mode = GST_SEARCH_MODE_BEFORE; } else if ((forward && (flags & GST_SEEK_FLAG_SNAP_AFTER)) || (!forward && (flags & GST_SEEK_FLAG_SNAP_BEFORE))) { mode = GST_SEARCH_MODE_AFTER; } else { mode = GST_SEARCH_MODE_BEFORE; } entry = gst_util_array_binary_search (sidx->entries, sidx->entries_count, sizeof (GstSidxBoxEntry), (GCompareDataFunc) gst_dash_demux_index_entry_search, mode, &ts, NULL); idx = entry - sidx->entries; /* FIXME in reverse mode, if we are exactly at a fragment start it makes more * sense to start from the end of the previous fragment */ /* FIXME we should have a GST_SEARCH_MODE_NEAREST */ if ((flags & GST_SEEK_FLAG_SNAP_NEAREST) == GST_SEEK_FLAG_SNAP_NEAREST && idx + 1 < sidx->entries_count) { if (ABS (sidx->entries[idx + 1].pts - ts) < ABS (sidx->entries[idx].pts - ts)) idx += 1; } } sidx->entry_index = idx; dashstream->sidx_index = idx; if (final_ts) { if (idx == sidx->entries_count) *final_ts = sidx->entries[idx].pts + sidx->entries[idx].duration; else *final_ts = sidx->entries[idx].pts; } } static GstFlowReturn gst_dash_demux_stream_seek (GstAdaptiveDemuxStream * stream, gboolean forward, GstSeekFlags flags, GstClockTime ts, GstClockTime * final_ts) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (stream->demux); if (dashstream->moof) gst_isoff_moof_box_free (dashstream->moof); dashstream->moof = NULL; if (dashstream->moof_sync_samples) g_array_free (dashstream->moof_sync_samples, TRUE); dashstream->moof_sync_samples = NULL; dashstream->current_sync_sample = -1; if (gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client)) { if (dashstream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) { gst_dash_demux_stream_sidx_seek (dashstream, forward, flags, ts, final_ts); } else { /* no index yet, seek when we have it */ /* FIXME - the final_ts won't be correct here */ dashstream->pending_seek_ts = ts; } return GST_FLOW_OK; } gst_mpd_client_stream_seek (dashdemux->client, dashstream->active_stream, forward, flags, ts, final_ts); return GST_FLOW_OK; } static gboolean gst_dash_demux_stream_has_next_sync_sample (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; if (dashstream->moof_sync_samples && GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) { if (stream->demux->segment.rate > 0.0) { if (dashstream->current_sync_sample + 1 < dashstream->moof_sync_samples->len) return TRUE; } else { if (dashstream->current_sync_sample >= 1) return TRUE; } } return FALSE; } static gboolean gst_dash_demux_stream_has_next_subfragment (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstSidxBox *sidx = SIDX (dashstream); if (dashstream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) { if (stream->demux->segment.rate > 0.0) { if (sidx->entry_index + 1 < sidx->entries_count) return TRUE; } else { if (sidx->entry_index >= 1) return TRUE; } } return FALSE; } static gboolean gst_dash_demux_stream_advance_sync_sample (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; gboolean fragment_finished = FALSE; if (dashstream->moof_sync_samples && GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) { if (stream->demux->segment.rate > 0.0) { dashstream->current_sync_sample++; if (dashstream->current_sync_sample >= dashstream->moof_sync_samples->len) { fragment_finished = TRUE; } } else { if (dashstream->current_sync_sample == -1) { dashstream->current_sync_sample = dashstream->moof_sync_samples->len - 1; } else if (dashstream->current_sync_sample == 0) { dashstream->current_sync_sample = -1; fragment_finished = TRUE; } else { dashstream->current_sync_sample--; } } } GST_DEBUG_OBJECT (stream->pad, "Advancing sync sample #%d fragment_finished:%d", dashstream->current_sync_sample, fragment_finished); if (!fragment_finished) stream->discont = TRUE; return !fragment_finished; } static gboolean gst_dash_demux_stream_advance_subfragment (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstSidxBox *sidx = SIDX (dashstream); gboolean fragment_finished = TRUE; if (dashstream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) { if (stream->demux->segment.rate > 0.0) { sidx->entry_index++; if (sidx->entry_index < sidx->entries_count) { fragment_finished = FALSE; } dashstream->sidx_index++; } else { sidx->entry_index--; dashstream->sidx_index--; if (sidx->entry_index >= 0) { fragment_finished = FALSE; } } } GST_DEBUG_OBJECT (stream->pad, "New sidx index: %d / %d. " "Finished fragment: %d", sidx->entry_index, sidx->entries_count, fragment_finished); return !fragment_finished; } static gboolean gst_dash_demux_stream_has_next_fragment (GstAdaptiveDemuxStream * stream) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (stream->demux); GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; if (dashstream->moof_sync_samples && GST_ADAPTIVE_DEMUX (dashdemux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) { if (gst_dash_demux_stream_has_next_sync_sample (stream)) return TRUE; } if (gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client)) { if (gst_dash_demux_stream_has_next_subfragment (stream)) return TRUE; } return gst_mpd_client_has_next_segment (dashdemux->client, dashstream->active_stream, stream->demux->segment.rate > 0.0); } static void gst_dash_demux_clear_pending_stream_data (GstDashDemux * dashdemux, GstDashDemuxStream * dashstream) { gst_isoff_sidx_parser_clear (&dashstream->sidx_parser); gst_isoff_sidx_parser_init (&dashstream->sidx_parser); if (dashstream->sidx_adapter) gst_adapter_clear (dashstream->sidx_adapter); /* Reset ISOBMFF box parsing state */ if (dashstream->isobmff_adapter) gst_adapter_clear (dashstream->isobmff_adapter); dashstream->isobmff_parser.current_fourcc = 0; dashstream->isobmff_parser.current_start_offset = 0; dashstream->isobmff_parser.current_offset = 0; dashstream->isobmff_parser.current_size = 0; if (dashstream->moof) gst_isoff_moof_box_free (dashstream->moof); dashstream->moof = NULL; if (dashstream->moof_sync_samples) g_array_free (dashstream->moof_sync_samples, TRUE); dashstream->moof_sync_samples = NULL; dashstream->current_sync_sample = -1; } static GstFlowReturn gst_dash_demux_stream_advance_fragment (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (stream->demux); GST_DEBUG_OBJECT (stream->pad, "Advance fragment"); /* If downloading only keyframes, switch to the next one or fall through */ if (dashstream->moof_sync_samples && GST_ADAPTIVE_DEMUX (dashdemux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) { if (gst_dash_demux_stream_advance_sync_sample (stream)) return GST_FLOW_OK; } if (dashstream->isobmff_adapter) gst_adapter_clear (dashstream->isobmff_adapter); dashstream->isobmff_parser.current_fourcc = 0; dashstream->isobmff_parser.current_start_offset = 0; dashstream->isobmff_parser.current_offset = 0; dashstream->isobmff_parser.current_size = 0; if (dashstream->moof) gst_isoff_moof_box_free (dashstream->moof); dashstream->moof = NULL; if (dashstream->moof_sync_samples) g_array_free (dashstream->moof_sync_samples, TRUE); dashstream->moof_sync_samples = NULL; dashstream->current_sync_sample = -1; if (gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client)) { if (gst_dash_demux_stream_advance_subfragment (stream)) return GST_FLOW_OK; } gst_isoff_sidx_parser_clear (&dashstream->sidx_parser); gst_isoff_sidx_parser_init (&dashstream->sidx_parser); if (dashstream->sidx_adapter) gst_adapter_clear (dashstream->sidx_adapter); return gst_mpd_client_advance_segment (dashdemux->client, dashstream->active_stream, stream->demux->segment.rate > 0.0); } static gboolean gst_dash_demux_stream_select_bitrate (GstAdaptiveDemuxStream * stream, guint64 bitrate) { GstActiveStream *active_stream = NULL; GList *rep_list = NULL; gint new_index; GstAdaptiveDemux *base_demux = stream->demux; GstDashDemux *demux = GST_DASH_DEMUX_CAST (stream->demux); GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; gboolean ret = FALSE; active_stream = dashstream->active_stream; if (active_stream == NULL) { goto end; } /* retrieve representation list */ if (active_stream->cur_adapt_set) rep_list = active_stream->cur_adapt_set->Representations; if (!rep_list) { goto end; } GST_DEBUG_OBJECT (stream->pad, "Trying to change to bitrate: %" G_GUINT64_FORMAT, bitrate); if (active_stream->mimeType == GST_STREAM_VIDEO && demux->max_bitrate) { bitrate = MIN (demux->max_bitrate, bitrate); } /* get representation index with current max_bandwidth */ if ((base_demux->segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) || ABS (base_demux->segment.rate) <= 1.0) { new_index = gst_mpdparser_get_rep_idx_with_max_bandwidth (rep_list, bitrate, demux->max_video_width, demux->max_video_height, demux->max_video_framerate_n, demux->max_video_framerate_d); } else { new_index = gst_mpdparser_get_rep_idx_with_max_bandwidth (rep_list, bitrate / ABS (base_demux->segment.rate), demux->max_video_width, demux->max_video_height, demux->max_video_framerate_n, demux->max_video_framerate_d); } /* if no representation has the required bandwidth, take the lowest one */ if (new_index == -1) new_index = gst_mpdparser_get_rep_idx_with_min_bandwidth (rep_list); if (new_index != active_stream->representation_idx) { GstRepresentationNode *rep = g_list_nth_data (rep_list, new_index); GST_INFO_OBJECT (demux, "Changing representation idx: %d %d %u", dashstream->index, new_index, rep->bandwidth); if (gst_mpd_client_setup_representation (demux->client, active_stream, rep)) { GstCaps *caps; GST_INFO_OBJECT (demux, "Switching bitrate to %d", active_stream->cur_representation->bandwidth); caps = gst_dash_demux_get_input_caps (demux, active_stream); gst_adaptive_demux_stream_set_caps (stream, caps); ret = TRUE; } else { GST_WARNING_OBJECT (demux, "Can not switch representation, aborting..."); } } if (ret) { gst_dash_demux_clear_pending_stream_data (demux, dashstream); if (gst_mpd_client_has_isoff_ondemand_profile (demux->client)) { /* store our current position to change to the same one in a different * representation if needed */ dashstream->sidx_index = SIDX (dashstream)->entry_index; } } end: return 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)) static gboolean gst_dash_demux_seek (GstAdaptiveDemux * demux, GstEvent * seek) { gdouble rate; GstFormat format; GstSeekFlags flags; GstSeekType start_type, stop_type; gint64 start, stop; GList *list; GstClockTime current_pos, target_pos; guint current_period; GstStreamPeriod *period; GList *iter, *streams = NULL; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); gboolean trickmode_no_audio; 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; } if (demux->segment.rate > 0.0) { target_pos = (GstClockTime) start; } else { target_pos = (GstClockTime) stop; } /* select the requested Period in the Media Presentation */ if (!gst_mpd_client_setup_media_presentation (dashdemux->client, target_pos, -1, NULL)) return FALSE; current_period = 0; for (list = g_list_first (dashdemux->client->periods); list; list = g_list_next (list)) { period = list->data; current_pos = period->start; current_period = period->number; GST_DEBUG_OBJECT (demux, "Looking at period %u) start:%" GST_TIME_FORMAT " - duration:%" GST_TIME_FORMAT ") for position %" GST_TIME_FORMAT, current_period, GST_TIME_ARGS (current_pos), GST_TIME_ARGS (period->duration), GST_TIME_ARGS (target_pos)); if (current_pos <= target_pos && target_pos <= current_pos + period->duration) { break; } } if (list == NULL) { GST_WARNING_OBJECT (demux, "Could not find seeked Period"); return FALSE; } trickmode_no_audio = ! !(flags & GST_SEEK_FLAG_TRICKMODE_NO_AUDIO); streams = demux->streams; if (current_period != gst_mpd_client_get_period_index (dashdemux->client)) { GST_DEBUG_OBJECT (demux, "Seeking to Period %d", current_period); /* clean old active stream list, if any */ gst_active_streams_free (dashdemux->client); dashdemux->trickmode_no_audio = trickmode_no_audio; /* setup video, audio and subtitle streams, starting from the new Period */ if (!gst_mpd_client_set_period_index (dashdemux->client, current_period) || !gst_dash_demux_setup_all_streams (dashdemux)) return FALSE; streams = demux->next_streams; } else if (dashdemux->trickmode_no_audio != trickmode_no_audio) { /* clean old active stream list, if any */ gst_active_streams_free (dashdemux->client); dashdemux->trickmode_no_audio = trickmode_no_audio; /* setup video, audio and subtitle streams, starting from the new Period */ if (!gst_dash_demux_setup_all_streams (dashdemux)) return FALSE; streams = demux->next_streams; } /* Update the current sequence on all streams */ for (iter = streams; iter; iter = g_list_next (iter)) { GstDashDemuxStream *dashstream = iter->data; if (flags & GST_SEEK_FLAG_FLUSH) { gst_dash_demux_clear_pending_stream_data (dashdemux, dashstream); } gst_dash_demux_stream_seek (iter->data, rate >= 0, 0, target_pos, NULL); } return TRUE; } static gint64 gst_dash_demux_get_manifest_update_interval (GstAdaptiveDemux * demux) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); return MIN (dashdemux->client->mpd_node->minimumUpdatePeriod * 1000, SLOW_CLOCK_UPDATE_INTERVAL); } static GstFlowReturn gst_dash_demux_update_manifest_data (GstAdaptiveDemux * demux, GstBuffer * buffer) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); GstMpdClient *new_client = NULL; GstMapInfo mapinfo; GST_DEBUG_OBJECT (demux, "Updating manifest file from URL"); /* parse the manifest file */ new_client = gst_mpd_client_new (); gst_mpd_client_set_uri_downloader (new_client, demux->downloader); new_client->mpd_uri = g_strdup (demux->manifest_uri); new_client->mpd_base_uri = g_strdup (demux->manifest_base_uri); gst_buffer_map (buffer, &mapinfo, GST_MAP_READ); if (gst_mpd_parse (new_client, (gchar *) mapinfo.data, mapinfo.size)) { const gchar *period_id; guint period_idx; GList *iter; GList *streams_iter; /* prepare the new manifest and try to transfer the stream position * status from the old manifest client */ GST_DEBUG_OBJECT (demux, "Updating manifest"); period_id = gst_mpd_client_get_period_id (dashdemux->client); period_idx = gst_mpd_client_get_period_index (dashdemux->client); /* setup video, audio and subtitle streams, starting from current Period */ if (!gst_mpd_client_setup_media_presentation (new_client, -1, (period_id ? -1 : period_idx), period_id)) { /* TODO */ } if (period_id) { if (!gst_mpd_client_set_period_id (new_client, period_id)) { GST_DEBUG_OBJECT (demux, "Error setting up the updated manifest file"); gst_mpd_client_free (new_client); gst_buffer_unmap (buffer, &mapinfo); return GST_FLOW_EOS; } } else { if (!gst_mpd_client_set_period_index (new_client, period_idx)) { GST_DEBUG_OBJECT (demux, "Error setting up the updated manifest file"); gst_mpd_client_free (new_client); gst_buffer_unmap (buffer, &mapinfo); return GST_FLOW_EOS; } } if (!gst_dash_demux_setup_mpdparser_streams (dashdemux, new_client)) { GST_ERROR_OBJECT (demux, "Failed to setup streams on manifest " "update"); gst_mpd_client_free (new_client); gst_buffer_unmap (buffer, &mapinfo); return GST_FLOW_ERROR; } /* update the streams to play from the next segment */ for (iter = demux->streams, streams_iter = new_client->active_streams; iter && streams_iter; iter = g_list_next (iter), streams_iter = g_list_next (streams_iter)) { GstDashDemuxStream *demux_stream = iter->data; GstActiveStream *new_stream = streams_iter->data; GstClockTime ts; if (!new_stream) { GST_DEBUG_OBJECT (demux, "Stream of index %d is missing from manifest update", demux_stream->index); gst_mpd_client_free (new_client); gst_buffer_unmap (buffer, &mapinfo); return GST_FLOW_EOS; } if (gst_mpd_client_get_next_fragment_timestamp (dashdemux->client, demux_stream->index, &ts) || gst_mpd_client_get_last_fragment_timestamp_end (dashdemux->client, demux_stream->index, &ts)) { /* Due to rounding when doing the timescale conversions it might happen * that the ts falls back to a previous segment, leading the same data * to be downloaded twice. We try to work around this by always adding * 10 microseconds to get back to the correct segment. The errors are * usually on the order of nanoseconds so it should be enough. */ GST_DEBUG_OBJECT (GST_ADAPTIVE_DEMUX_STREAM_PAD (demux_stream), "Current position: %" GST_TIME_FORMAT ", updating to %" GST_TIME_FORMAT, GST_TIME_ARGS (ts), GST_TIME_ARGS (ts + (10 * GST_USECOND))); ts += 10 * GST_USECOND; gst_mpd_client_stream_seek (new_client, new_stream, demux->segment.rate >= 0, 0, ts, NULL); } demux_stream->active_stream = new_stream; } gst_mpd_client_free (dashdemux->client); dashdemux->client = new_client; GST_DEBUG_OBJECT (demux, "Manifest file successfully updated"); if (dashdemux->clock_drift) { gst_dash_demux_poll_clock_drift (dashdemux); } } else { /* 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 manifest */ GST_WARNING_OBJECT (demux, "Error parsing the manifest."); gst_mpd_client_free (new_client); gst_buffer_unmap (buffer, &mapinfo); return GST_FLOW_ERROR; } gst_buffer_unmap (buffer, &mapinfo); return GST_FLOW_OK; } static gint64 gst_dash_demux_stream_get_fragment_waiting_time (GstAdaptiveDemuxStream * stream) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (stream->demux); GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; GstDateTime *segmentAvailability; GstActiveStream *active_stream = dashstream->active_stream; segmentAvailability = gst_mpd_client_get_next_segment_availability_start_time (dashdemux->client, active_stream); if (segmentAvailability) { gint64 diff; GstDateTime *cur_time; cur_time = gst_date_time_new_from_g_date_time (gst_adaptive_demux_get_client_now_utc (GST_ADAPTIVE_DEMUX_CAST (dashdemux))); diff = gst_mpd_client_calculate_time_difference (cur_time, segmentAvailability); gst_date_time_unref (segmentAvailability); gst_date_time_unref (cur_time); /* subtract the server's clock drift, so that if the server's time is behind our idea of UTC, we need to sleep for longer before requesting a fragment */ return diff - gst_dash_demux_get_clock_compensation (dashdemux) * GST_USECOND; } return 0; } static gboolean gst_dash_demux_has_next_period (GstAdaptiveDemux * demux) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); if (demux->segment.rate >= 0) return gst_mpd_client_has_next_period (dashdemux->client); else return gst_mpd_client_has_previous_period (dashdemux->client); } static void gst_dash_demux_advance_period (GstAdaptiveDemux * demux) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); g_return_if_fail (gst_mpd_client_has_next_period (dashdemux->client)); if (demux->segment.rate >= 0) { if (!gst_mpd_client_set_period_index (dashdemux->client, gst_mpd_client_get_period_index (dashdemux->client) + 1)) { /* TODO error */ return; } } else { if (!gst_mpd_client_set_period_index (dashdemux->client, gst_mpd_client_get_period_index (dashdemux->client) - 1)) { /* TODO error */ return; } } gst_dash_demux_setup_all_streams (dashdemux); gst_mpd_client_seek_to_first_segment (dashdemux->client); } static GstBuffer * _gst_buffer_split (GstBuffer * buffer, gint offset, gsize size) { GstBuffer *newbuf = gst_buffer_copy_region (buffer, GST_BUFFER_COPY_FLAGS | GST_BUFFER_COPY_TIMESTAMPS | GST_BUFFER_COPY_META | GST_BUFFER_COPY_MEMORY, offset, size == -1 ? size : size - offset); gst_buffer_resize (buffer, 0, offset); return newbuf; } static gboolean gst_dash_demux_stream_fragment_start (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; dashstream->sidx_index_header_or_data = 0; dashstream->sidx_current_offset = -1; dashstream->isobmff_parser.index_header_or_data = 0; dashstream->isobmff_parser.current_offset = -1; /* We need to mark every first buffer of a key unit as discont, * and also every first buffer of a moov and moof. This ensures * that qtdemux takes note of our buffer offsets for each of those * buffers instead of keeping track of them itself from the first * buffer. We need offsets to be consistent between moof and mdat */ if (dashstream->is_isobmff && dashdemux->allow_trickmode_key_units && (demux->segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) && dashstream->active_stream->mimeType == GST_STREAM_VIDEO) stream->discont = TRUE; return TRUE; } static GstFlowReturn gst_dash_demux_stream_fragment_finished (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream) { GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; /* We need to mark every first buffer of a key unit as discont, * and also every first buffer of a moov and moof. This ensures * that qtdemux takes note of our buffer offsets for each of those * buffers instead of keeping track of them itself from the first * buffer. We need offsets to be consistent between moof and mdat */ if (dashstream->is_isobmff && dashdemux->allow_trickmode_key_units && (demux->segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) && dashstream->active_stream->mimeType == GST_STREAM_VIDEO) stream->discont = TRUE; /* Only handle fragment advancing specifically for SIDX if we're not * in key unit mode */ if (!(dashstream->moof_sync_samples && GST_ADAPTIVE_DEMUX (dashdemux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) && gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client) && dashstream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) { /* fragment is advanced on data_received when byte limits are reached */ if (gst_dash_demux_stream_has_next_fragment (stream)) return GST_FLOW_OK; return GST_FLOW_EOS; } if (G_UNLIKELY (stream->downloading_header || stream->downloading_index)) return GST_FLOW_OK; return gst_adaptive_demux_stream_advance_fragment (demux, stream, stream->fragment.duration); } static gboolean gst_dash_demux_need_another_chunk (GstAdaptiveDemuxStream * stream) { GstDashDemux *dashdemux = (GstDashDemux *) stream->demux; GstDashDemuxStream *dashstream = (GstDashDemuxStream *) stream; /* We're chunked downloading for ISOBMFF in KEY_UNITS mode for the actual * fragment until we parsed the moof and arrived at the mdat. 8192 is a * random guess for the moof size */ if (dashstream->is_isobmff && (GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) && dashstream->active_stream->mimeType == GST_STREAM_VIDEO && !stream->downloading_header && !stream->downloading_index && dashdemux->allow_trickmode_key_units) { if (dashstream->isobmff_parser.current_fourcc != GST_ISOFF_FOURCC_MDAT) { /* Need to download the moof first to know anything */ stream->fragment.chunk_size = 8192; /* Do we have the first fourcc already or are we in the middle */ if (dashstream->isobmff_parser.current_fourcc == 0) { stream->fragment.chunk_size += dashstream->moof_average_size; if (dashstream->first_sync_sample_always_after_moof) stream->fragment.chunk_size += dashstream->first_sync_sample_average_size; } if (gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client) && dashstream->sidx_parser.sidx.entries) { guint64 sidx_end_offset = dashstream->sidx_base_offset + SIDX_CURRENT_ENTRY (dashstream)->offset + SIDX_CURRENT_ENTRY (dashstream)->size; if (stream->fragment.chunk_size + dashstream->isobmff_parser.current_offset > sidx_end_offset) { stream->fragment.chunk_size = sidx_end_offset - dashstream->isobmff_parser.current_offset; } } } else if (dashstream->moof && dashstream->moof_sync_samples) { /* Have the moof, either we're done now or we want to download the * directly following sync sample */ if (dashstream->first_sync_sample_after_moof && dashstream->current_sync_sample == 0) { GstDashStreamSyncSample *sync_sample = &g_array_index (dashstream->moof_sync_samples, GstDashStreamSyncSample, 0); guint64 end_offset = sync_sample->end_offset + 1; if (gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client) && dashstream->sidx_parser.sidx.entries) { guint64 sidx_end_offset = dashstream->sidx_base_offset + SIDX_CURRENT_ENTRY (dashstream)->offset + SIDX_CURRENT_ENTRY (dashstream)->size; if (end_offset > sidx_end_offset) { end_offset = sidx_end_offset; } } if (dashstream->isobmff_parser.current_offset < end_offset) { stream->fragment.chunk_size = end_offset - dashstream->isobmff_parser.current_offset; } else { stream->fragment.chunk_size = 0; } } else { stream->fragment.chunk_size = 0; } } else { /* Have moof but can't do key-units mode, just download until the end */ stream->fragment.chunk_size = -1; } } else { /* We might've decided that we can't allow key-unit only * trickmodes while doing chunked downloading. In that case * just download from here to the end now */ if (dashstream->moof && (GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS)) stream->fragment.chunk_size = -1; else stream->fragment.chunk_size = 0; } return stream->fragment.chunk_size != 0; } static GstBuffer * gst_dash_demux_parse_isobmff (GstAdaptiveDemux * demux, GstDashDemuxStream * dash_stream, GstBuffer * buffer) { GstAdaptiveDemuxStream *stream = (GstAdaptiveDemuxStream *) dash_stream; gsize available; guint index_header_or_data; GstMapInfo map; GstByteReader reader; guint32 fourcc; guint header_size; guint64 size, buffer_offset; /* This must not be called when we're in the mdat. We only look at the mdat * header and then stop parsing the boxes as we're only interested in the * metadata! Handling mdat is the job of the surrounding code, as well as * stopping or starting the next fragment when mdat is over (=> sidx) */ g_assert (dash_stream->isobmff_parser.current_fourcc != GST_ISOFF_FOURCC_MDAT); if (stream->downloading_index) index_header_or_data = 1; else if (stream->downloading_header) index_header_or_data = 2; else index_header_or_data = 3; if (dash_stream->isobmff_parser.index_header_or_data != index_header_or_data) { /* Clear pending data */ if (gst_adapter_available (dash_stream->isobmff_adapter) != 0) GST_ERROR_OBJECT (stream->pad, "Had pending ISOBMFF data after switch between index/header/data"); gst_adapter_clear (dash_stream->isobmff_adapter); dash_stream->isobmff_parser.current_fourcc = 0; dash_stream->isobmff_parser.current_start_offset = 0; dash_stream->isobmff_parser.current_offset = -1; dash_stream->isobmff_parser.current_size = 0; dash_stream->isobmff_parser.index_header_or_data = index_header_or_data; } if (dash_stream->isobmff_parser.current_offset == -1) { dash_stream->isobmff_parser.current_offset = GST_BUFFER_OFFSET_IS_VALID (buffer) ? GST_BUFFER_OFFSET (buffer) : 0; } gst_adapter_push (dash_stream->isobmff_adapter, buffer); available = gst_adapter_available (dash_stream->isobmff_adapter); buffer = gst_adapter_take_buffer (dash_stream->isobmff_adapter, available); buffer_offset = dash_stream->isobmff_parser.current_offset; /* Always at the start of a box here */ g_assert (dash_stream->isobmff_parser.current_size == 0); /* At the start of a box => Parse it */ gst_buffer_map (buffer, &map, GST_MAP_READ); gst_byte_reader_init (&reader, map.data, map.size); /* While there are more boxes left to parse ... */ dash_stream->isobmff_parser.current_start_offset = dash_stream->isobmff_parser.current_offset; do { dash_stream->isobmff_parser.current_fourcc = 0; dash_stream->isobmff_parser.current_size = 0; if (!gst_isoff_parse_box_header (&reader, &fourcc, NULL, &header_size, &size)) { break; } dash_stream->isobmff_parser.current_fourcc = fourcc; if (size == 0) { /* We assume this is mdat, anything else with "size until end" * does not seem to make sense */ g_assert (dash_stream->isobmff_parser.current_fourcc == GST_ISOFF_FOURCC_MDAT); dash_stream->isobmff_parser.current_size = -1; break; } dash_stream->isobmff_parser.current_size = size; /* Do we have the complete box or are at MDAT */ if (gst_byte_reader_get_remaining (&reader) < size - header_size || dash_stream->isobmff_parser.current_fourcc == GST_ISOFF_FOURCC_MDAT) { /* Reset byte reader to the beginning of the box */ gst_byte_reader_set_pos (&reader, gst_byte_reader_get_pos (&reader) - header_size); break; } GST_LOG_OBJECT (stream->pad, "box %" GST_FOURCC_FORMAT " at offset %" G_GUINT64_FORMAT " size %" G_GUINT64_FORMAT, GST_FOURCC_ARGS (fourcc), dash_stream->isobmff_parser.current_offset + gst_byte_reader_get_pos (&reader) - header_size, size); if (dash_stream->isobmff_parser.current_fourcc == GST_ISOFF_FOURCC_MOOF) { GstByteReader sub_reader; g_assert (dash_stream->moof == NULL); g_assert (dash_stream->moof_sync_samples == NULL); gst_byte_reader_get_sub_reader (&reader, &sub_reader, size - header_size); dash_stream->moof = gst_isoff_moof_box_parse (&sub_reader); dash_stream->moof_offset = dash_stream->isobmff_parser.current_start_offset; dash_stream->moof_size = size; dash_stream->current_sync_sample = -1; if (dash_stream->moof_average_size) { if (dash_stream->moof_average_size < size) dash_stream->moof_average_size = (size * 3 + dash_stream->moof_average_size) / 4; else dash_stream->moof_average_size = (size + dash_stream->moof_average_size + 3) / 4; } else { dash_stream->moof_average_size = size; } } else { gst_byte_reader_skip (&reader, size - header_size); } dash_stream->isobmff_parser.current_fourcc = 0; dash_stream->isobmff_parser.current_start_offset += size; dash_stream->isobmff_parser.current_size = 0; } while (gst_byte_reader_get_remaining (&reader) > 0); gst_buffer_unmap (buffer, &map); /* mdat? Push all we have and wait for it to be over */ if (dash_stream->isobmff_parser.current_fourcc == GST_ISOFF_FOURCC_MDAT) { GstBuffer *pending; GST_LOG_OBJECT (stream->pad, "box %" GST_FOURCC_FORMAT " at offset %" G_GUINT64_FORMAT " size %" G_GUINT64_FORMAT, GST_FOURCC_ARGS (fourcc), dash_stream->isobmff_parser.current_offset + gst_byte_reader_get_pos (&reader) - header_size, dash_stream->isobmff_parser.current_size); /* At mdat. Move the start of the mdat to the adapter and have everything * else be pushed. We parsed all header boxes at this point and are not * supposed to be called again until the next moof */ pending = _gst_buffer_split (buffer, gst_byte_reader_get_pos (&reader), -1); gst_adapter_push (dash_stream->isobmff_adapter, pending); dash_stream->isobmff_parser.current_offset += gst_byte_reader_get_pos (&reader); dash_stream->isobmff_parser.current_size = 0; GST_BUFFER_OFFSET (buffer) = buffer_offset; GST_BUFFER_OFFSET_END (buffer) = buffer_offset + gst_buffer_get_size (buffer); return buffer; } else if (gst_byte_reader_get_pos (&reader) != 0) { GstBuffer *pending; /* Multiple complete boxes and no mdat? Push them and keep the remainder, * which is the start of the next box if any remainder */ pending = _gst_buffer_split (buffer, gst_byte_reader_get_pos (&reader), -1); gst_adapter_push (dash_stream->isobmff_adapter, pending); dash_stream->isobmff_parser.current_offset += gst_byte_reader_get_pos (&reader); dash_stream->isobmff_parser.current_size = 0; GST_BUFFER_OFFSET (buffer) = buffer_offset; GST_BUFFER_OFFSET_END (buffer) = buffer_offset + gst_buffer_get_size (buffer); return buffer; } /* Not even a single complete, non-mdat box, wait */ dash_stream->isobmff_parser.current_size = 0; gst_adapter_push (dash_stream->isobmff_adapter, buffer); return NULL; } static gboolean gst_dash_demux_find_sync_samples (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream) { GstDashDemux *dashdemux = (GstDashDemux *) stream->demux; GstDashDemuxStream *dash_stream = (GstDashDemuxStream *) stream; guint i; guint32 track_id = 0; guint64 prev_traf_end; gboolean trex_sample_flags = FALSE; if (!dash_stream->moof) { dashdemux->allow_trickmode_key_units = FALSE; return FALSE; } dash_stream->current_sync_sample = -1; dash_stream->moof_sync_samples = g_array_new (FALSE, FALSE, sizeof (GstDashStreamSyncSample)); prev_traf_end = dash_stream->moof_offset; /* generate table of keyframes and offsets */ for (i = 0; i < dash_stream->moof->traf->len; i++) { GstTrafBox *traf = &g_array_index (dash_stream->moof->traf, GstTrafBox, i); guint64 traf_offset = 0, prev_trun_end; guint j; if (i == 0) { track_id = traf->tfhd.track_id; } else if (track_id != traf->tfhd.track_id) { GST_ERROR_OBJECT (stream->pad, "moof with trafs of different track ids (%u != %u)", track_id, traf->tfhd.track_id); g_array_free (dash_stream->moof_sync_samples, TRUE); dash_stream->moof_sync_samples = NULL; dashdemux->allow_trickmode_key_units = FALSE; return FALSE; } if (traf->tfhd.flags & GST_TFHD_FLAGS_BASE_DATA_OFFSET_PRESENT) { traf_offset = traf->tfhd.base_data_offset; } else if (traf->tfhd.flags & GST_TFHD_FLAGS_DEFAULT_BASE_IS_MOOF) { traf_offset = dash_stream->moof_offset; } else { traf_offset = prev_traf_end; } prev_trun_end = traf_offset; for (j = 0; j < traf->trun->len; j++) { GstTrunBox *trun = &g_array_index (traf->trun, GstTrunBox, j); guint64 trun_offset, prev_sample_end; guint k; if (trun->flags & GST_TRUN_FLAGS_DATA_OFFSET_PRESENT) { trun_offset = traf_offset + trun->data_offset; } else { trun_offset = prev_trun_end; } prev_sample_end = trun_offset; for (k = 0; k < trun->samples->len; k++) { GstTrunSample *sample = &g_array_index (trun->samples, GstTrunSample, k); guint64 sample_offset; guint32 sample_flags; #if 0 guint32 sample_duration; #endif sample_offset = prev_sample_end; if (trun->flags & GST_TRUN_FLAGS_SAMPLE_FLAGS_PRESENT) { sample_flags = sample->sample_flags; } else if ((trun->flags & GST_TRUN_FLAGS_FIRST_SAMPLE_FLAGS_PRESENT) && k == 0) { sample_flags = trun->first_sample_flags; } else if (traf-> tfhd.flags & GST_TFHD_FLAGS_DEFAULT_SAMPLE_FLAGS_PRESENT) { sample_flags = traf->tfhd.default_sample_flags; } else { trex_sample_flags = TRUE; continue; } #if 0 if (trun->flags & GST_TRUN_FLAGS_SAMPLE_DURATION_PRESENT) { sample_duration = sample->sample_duration; } else if (traf-> tfhd.flags & GST_TFHD_FLAGS_DEFAULT_SAMPLE_DURATION_PRESENT) { sample_duration = traf->tfhd.default_sample_duration; } else { GST_FIXME_OBJECT (stream->pad, "Sample duration given by trex - can't download only keyframes"); g_array_free (dash_stream->moof_sync_samples, TRUE); dash_stream->moof_sync_samples = NULL; return FALSE; } #endif if (trun->flags & GST_TRUN_FLAGS_SAMPLE_SIZE_PRESENT) { prev_sample_end += sample->sample_size; } else if (traf-> tfhd.flags & GST_TFHD_FLAGS_DEFAULT_SAMPLE_SIZE_PRESENT) { prev_sample_end += traf->tfhd.default_sample_size; } else { GST_FIXME_OBJECT (stream->pad, "Sample size given by trex - can't download only keyframes"); g_array_free (dash_stream->moof_sync_samples, TRUE); dash_stream->moof_sync_samples = NULL; dashdemux->allow_trickmode_key_units = FALSE; return FALSE; } /* Non-non-sync sample aka sync sample */ if (!GST_ISOFF_SAMPLE_FLAGS_SAMPLE_IS_NON_SYNC_SAMPLE (sample_flags) || GST_ISOFF_SAMPLE_FLAGS_SAMPLE_DEPENDS_ON (sample_flags) == 2) { GstDashStreamSyncSample sync_sample = { sample_offset, prev_sample_end - 1 }; /* TODO: need timestamps so we can decide to download or not */ g_array_append_val (dash_stream->moof_sync_samples, sync_sample); } } prev_trun_end = prev_sample_end; } prev_traf_end = prev_trun_end; } if (trex_sample_flags) { if (dash_stream->moof_sync_samples->len > 0) { GST_LOG_OBJECT (stream->pad, "Some sample flags given by trex but still found sync samples"); } else { GST_FIXME_OBJECT (stream->pad, "Sample flags given by trex - can't download only keyframes"); g_array_free (dash_stream->moof_sync_samples, TRUE); dash_stream->moof_sync_samples = NULL; dashdemux->allow_trickmode_key_units = FALSE; return FALSE; } } if (dash_stream->moof_sync_samples->len == 0) { GST_LOG_OBJECT (stream->pad, "No sync samples found in fragment"); g_array_free (dash_stream->moof_sync_samples, TRUE); dash_stream->moof_sync_samples = NULL; dashdemux->allow_trickmode_key_units = FALSE; return FALSE; } { GstDashStreamSyncSample *sync_sample = &g_array_index (dash_stream->moof_sync_samples, GstDashStreamSyncSample, 0); guint size = sync_sample->end_offset + 1 - sync_sample->start_offset; if (dash_stream->first_sync_sample_average_size) { if (dash_stream->first_sync_sample_average_size < size) dash_stream->first_sync_sample_average_size = (size * 3 + dash_stream->first_sync_sample_average_size) / 4; else dash_stream->first_sync_sample_average_size = (size + dash_stream->first_sync_sample_average_size * 3) / 4; } else { dash_stream->first_sync_sample_average_size = size; } if (dash_stream->moof_offset + dash_stream->moof_size + 8 < sync_sample->start_offset) { dash_stream->first_sync_sample_after_moof = FALSE; dash_stream->first_sync_sample_always_after_moof = FALSE; } else { dash_stream->first_sync_sample_after_moof = (dash_stream->moof_sync_samples->len == 1 || demux->segment.rate > 0.0); } } return TRUE; } static GstFlowReturn gst_dash_demux_handle_isobmff_buffer (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream, GstBuffer * buffer) { GstDashDemuxStream *dash_stream = (GstDashDemuxStream *) stream; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); GstFlowReturn ret = GST_FLOW_OK; if (dashdemux->allow_trickmode_key_units) { if (dash_stream->isobmff_parser.current_fourcc != GST_ISOFF_FOURCC_MDAT) { buffer = gst_dash_demux_parse_isobmff (demux, dash_stream, buffer); if (buffer && (ret = gst_adaptive_demux_stream_push_buffer (stream, buffer)) != GST_FLOW_OK) return ret; if (dash_stream->isobmff_parser.current_fourcc != GST_ISOFF_FOURCC_MDAT) return ret; /* Jump to the next sync sample. As we're doing chunked downloading * here, just drop data until our chunk is over so we can reuse the * HTTP connection instead of having to create a new one or * reuse the data if the sync sample follows the moof */ if (dash_stream->active_stream->mimeType == GST_STREAM_VIDEO && gst_dash_demux_find_sync_samples (demux, stream) && GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) { if (dash_stream->first_sync_sample_after_moof) { /* If we're here, don't throw away data but collect sync * sample while we're at it below. We're doing chunked * downloading so might need to adjust the next chunk size for * the remainder */ dash_stream->current_sync_sample = 0; } else { gst_adapter_clear (dash_stream->isobmff_adapter); } } if (gst_adapter_available (dash_stream->isobmff_adapter) == 0) return ret; /* We have some data from the mdat available in the adapter, handle it * below in the push code */ buffer = gst_adapter_take_buffer (dash_stream->isobmff_adapter, gst_adapter_available (dash_stream->isobmff_adapter)); } else { if (dash_stream->isobmff_parser.current_offset == -1) { dash_stream->isobmff_parser.current_offset = GST_BUFFER_OFFSET_IS_VALID (buffer) ? GST_BUFFER_OFFSET (buffer) : 0; } } /* At mdat and isobmff and trick modes are allowed */ /* We're actually running in key-units trick mode */ if (dash_stream->active_stream->mimeType == GST_STREAM_VIDEO && dash_stream->moof_sync_samples && GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS) { if (dash_stream->current_sync_sample == -1) { /* We're doing chunked downloading and wait for finishing the current * chunk so we can jump to the first keyframe */ gst_buffer_unref (buffer); return GST_FLOW_OK; } else if (dash_stream->first_sync_sample_after_moof && dash_stream->current_sync_sample == 0) { GstDashStreamSyncSample *sync_sample = &g_array_index (dash_stream->moof_sync_samples, GstDashStreamSyncSample, 0); guint64 end_offset = dash_stream->isobmff_parser.current_offset + gst_buffer_get_size (buffer); /* If the first keyframe follows directly the moof and we're * downloading it here, make sure to not download too much */ if (end_offset > sync_sample->end_offset + 1) { guint64 remaining = sync_sample->end_offset + 1 - dash_stream->isobmff_parser.current_offset; if (remaining) { GstBuffer *sub = gst_buffer_copy_region (buffer, GST_BUFFER_COPY_ALL, 0, remaining); gst_buffer_unref (buffer); buffer = sub; } else { gst_buffer_unref (buffer); return GST_FLOW_OK; } } } } GST_BUFFER_OFFSET (buffer) = dash_stream->isobmff_parser.current_offset; dash_stream->isobmff_parser.current_offset += gst_buffer_get_size (buffer); GST_BUFFER_OFFSET_END (buffer) = dash_stream->isobmff_parser.current_offset; } return gst_adaptive_demux_stream_push_buffer (stream, buffer); } static GstFlowReturn gst_dash_demux_data_received (GstAdaptiveDemux * demux, GstAdaptiveDemuxStream * stream, GstBuffer * buffer) { GstDashDemuxStream *dash_stream = (GstDashDemuxStream *) stream; GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux); GstFlowReturn ret = GST_FLOW_OK; guint index_header_or_data; if (!gst_mpd_client_has_isoff_ondemand_profile (dashdemux->client)) { if (dash_stream->is_isobmff) return gst_dash_demux_handle_isobmff_buffer (demux, stream, buffer); else return gst_adaptive_demux_stream_push_buffer (stream, buffer); } if (stream->downloading_index) index_header_or_data = 1; else if (stream->downloading_header) index_header_or_data = 2; else index_header_or_data = 3; if (dash_stream->sidx_index_header_or_data != index_header_or_data) { /* Clear pending data */ if (gst_adapter_available (dash_stream->sidx_adapter) != 0) GST_ERROR_OBJECT (stream->pad, "Had pending SIDX data after switch between index/header/data"); gst_adapter_clear (dash_stream->sidx_adapter); dash_stream->sidx_index_header_or_data = index_header_or_data; dash_stream->sidx_current_offset = -1; } if (dash_stream->sidx_current_offset == -1) dash_stream->sidx_current_offset = GST_BUFFER_OFFSET_IS_VALID (buffer) ? GST_BUFFER_OFFSET (buffer) : 0; gst_adapter_push (dash_stream->sidx_adapter, buffer); buffer = NULL; if (stream->downloading_index) { GstIsoffParserResult res; guint consumed; gsize available; available = gst_adapter_available (dash_stream->sidx_adapter); buffer = gst_adapter_take_buffer (dash_stream->sidx_adapter, available); if (dash_stream->sidx_parser.status != GST_ISOFF_SIDX_PARSER_FINISHED) { res = gst_isoff_sidx_parser_add_buffer (&dash_stream->sidx_parser, buffer, &consumed); if (res == GST_ISOFF_PARSER_ERROR) { } else if (res == GST_ISOFF_PARSER_UNEXPECTED) { /* this is not a 'sidx' index, just skip it and continue playback */ } else { /* when finished, prepare for real data streaming */ if (dash_stream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) { if (GST_CLOCK_TIME_IS_VALID (dash_stream->pending_seek_ts)) { /* FIXME, preserve seek flags */ gst_dash_demux_stream_sidx_seek (dash_stream, demux->segment.rate >= 0, 0, dash_stream->pending_seek_ts, NULL); dash_stream->pending_seek_ts = GST_CLOCK_TIME_NONE; } else { SIDX (dash_stream)->entry_index = dash_stream->sidx_index; } } else if (consumed < available) { GstBuffer *pending; /* we still need to keep some data around for the next parsing round * so just push what was already processed by the parser */ pending = _gst_buffer_split (buffer, consumed, -1); gst_adapter_push (dash_stream->sidx_adapter, pending); } } } GST_BUFFER_OFFSET (buffer) = dash_stream->sidx_current_offset; GST_BUFFER_OFFSET_END (buffer) = GST_BUFFER_OFFSET (buffer) + gst_buffer_get_size (buffer); dash_stream->sidx_current_offset = GST_BUFFER_OFFSET_END (buffer); ret = gst_adaptive_demux_stream_push_buffer (stream, buffer); } else if (dash_stream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) { gsize available; while (ret == GST_FLOW_OK && ((available = gst_adapter_available (dash_stream->sidx_adapter)) > 0)) { gboolean advance = FALSE; guint64 sidx_end_offset = dash_stream->sidx_base_offset + SIDX_CURRENT_ENTRY (dash_stream)->offset + SIDX_CURRENT_ENTRY (dash_stream)->size; if (dash_stream->sidx_current_offset + available < sidx_end_offset) { buffer = gst_adapter_take_buffer (dash_stream->sidx_adapter, available); } else { /* In key-units mode, we advance the fragments manually once the * current key-unit is over but throw away all data that is after * the current sidx entry end */ if (dash_stream->moof_sync_samples && (GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS)) { if (sidx_end_offset <= dash_stream->sidx_current_offset) { buffer = NULL; gst_adapter_clear (dash_stream->sidx_adapter); } else { buffer = gst_adapter_take_buffer (dash_stream->sidx_adapter, sidx_end_offset - dash_stream->sidx_current_offset); } } else { buffer = gst_adapter_take_buffer (dash_stream->sidx_adapter, sidx_end_offset - dash_stream->sidx_current_offset); advance = TRUE; } } if (buffer != NULL) { GST_BUFFER_OFFSET (buffer) = dash_stream->sidx_current_offset; GST_BUFFER_OFFSET_END (buffer) = GST_BUFFER_OFFSET (buffer) + gst_buffer_get_size (buffer); dash_stream->sidx_current_offset = GST_BUFFER_OFFSET_END (buffer); if (dash_stream->is_isobmff) ret = gst_dash_demux_handle_isobmff_buffer (demux, stream, buffer); else ret = gst_adaptive_demux_stream_push_buffer (stream, buffer); if (!(dash_stream->moof_sync_samples && (GST_ADAPTIVE_DEMUX (stream->demux)-> segment.flags & GST_SEGMENT_FLAG_TRICKMODE_KEY_UNITS)) && advance) { GstFlowReturn new_ret; new_ret = gst_adaptive_demux_stream_advance_fragment (demux, stream, SIDX_CURRENT_ENTRY (dash_stream)->duration); /* only overwrite if it was OK before */ if (ret == GST_FLOW_OK) ret = new_ret; } } } } else { /* this should be the main header, just push it all */ buffer = gst_adapter_take_buffer (dash_stream->sidx_adapter, gst_adapter_available (dash_stream->sidx_adapter)); GST_BUFFER_OFFSET (buffer) = dash_stream->sidx_current_offset; GST_BUFFER_OFFSET_END (buffer) = GST_BUFFER_OFFSET (buffer) + gst_buffer_get_size (buffer); dash_stream->sidx_current_offset = GST_BUFFER_OFFSET_END (buffer); if (dash_stream->is_isobmff) return gst_dash_demux_handle_isobmff_buffer (demux, stream, buffer); else return gst_adaptive_demux_stream_push_buffer (stream, buffer); } return ret; } static void gst_dash_demux_stream_free (GstAdaptiveDemuxStream * stream) { GstDashDemuxStream *dash_stream = (GstDashDemuxStream *) stream; gst_isoff_sidx_parser_clear (&dash_stream->sidx_parser); if (dash_stream->sidx_adapter) g_object_unref (dash_stream->sidx_adapter); if (dash_stream->isobmff_adapter) g_object_unref (dash_stream->isobmff_adapter); } static GstDashDemuxClockDrift * gst_dash_demux_clock_drift_new (GstDashDemux * demux) { GstDashDemuxClockDrift *clock_drift; clock_drift = g_slice_new0 (GstDashDemuxClockDrift); g_mutex_init (&clock_drift->clock_lock); clock_drift->next_update = GST_TIME_AS_USECONDS (gst_adaptive_demux_get_monotonic_time (GST_ADAPTIVE_DEMUX_CAST (demux))); return clock_drift; } static void gst_dash_demux_clock_drift_free (GstDashDemuxClockDrift * clock_drift) { if (clock_drift) { g_mutex_lock (&clock_drift->clock_lock); if (clock_drift->ntp_clock) g_object_unref (clock_drift->ntp_clock); g_mutex_unlock (&clock_drift->clock_lock); g_mutex_clear (&clock_drift->clock_lock); g_slice_free (GstDashDemuxClockDrift, clock_drift); } } /* * The value attribute of the UTCTiming element contains a white-space * separated list of servers that are recommended to be used in * combination with the NTP protocol as defined in IETF RFC 5905 for * getting the appropriate time. * * The DASH standard does not specify which version of NTP. This * function only works with NTPv4 servers. */ static GstDateTime * gst_dash_demux_poll_ntp_server (GstDashDemuxClockDrift * clock_drift, gchar ** urls) { GstClockTime ntp_clock_time; GDateTime *dt, *dt2; if (!clock_drift->ntp_clock) { GResolver *resolver; GList *inet_addrs; GError *err; gchar *ip_addr; resolver = g_resolver_get_default (); /* We don't round-robin NTP servers. If the manifest specifies multiple NTP time servers, select one at random */ clock_drift->selected_url = g_random_int_range (0, g_strv_length (urls)); GST_DEBUG ("Connecting to NTP time server %s", urls[clock_drift->selected_url]); inet_addrs = g_resolver_lookup_by_name (resolver, urls[clock_drift->selected_url], NULL, &err); g_object_unref (resolver); if (!inet_addrs || g_list_length (inet_addrs) == 0) { GST_ERROR ("Failed to resolve hostname of NTP server: %s", err ? (err->message) : "unknown error"); if (inet_addrs) g_resolver_free_addresses (inet_addrs); if (err) g_error_free (err); return NULL; } ip_addr = g_inet_address_to_string ((GInetAddress *) (g_list_first (inet_addrs)->data)); clock_drift->ntp_clock = gst_ntp_clock_new ("dashntp", ip_addr, 123, 0); g_free (ip_addr); g_resolver_free_addresses (inet_addrs); if (!clock_drift->ntp_clock) { GST_ERROR ("Failed to create NTP clock"); return NULL; } if (!gst_clock_wait_for_sync (clock_drift->ntp_clock, 5 * GST_SECOND)) { g_object_unref (clock_drift->ntp_clock); clock_drift->ntp_clock = NULL; GST_ERROR ("Failed to lock to NTP clock"); return NULL; } } ntp_clock_time = gst_clock_get_time (clock_drift->ntp_clock); if (ntp_clock_time == GST_CLOCK_TIME_NONE) { GST_ERROR ("Failed to get time from NTP clock"); return NULL; } ntp_clock_time -= NTP_TO_UNIX_EPOCH * GST_SECOND; dt = g_date_time_new_from_unix_utc (ntp_clock_time / GST_SECOND); if (!dt) { GST_ERROR ("Failed to create GstDateTime"); return NULL; } ntp_clock_time = gst_util_uint64_scale (ntp_clock_time % GST_SECOND, 1000000, GST_SECOND); dt2 = g_date_time_add (dt, ntp_clock_time); g_date_time_unref (dt); return gst_date_time_new_from_g_date_time (dt2); } struct Rfc5322TimeZone { const gchar *name; gfloat tzoffset; }; /* Parse an RFC5322 (section 3.3) date-time from the Date: field in the HTTP response. See https://tools.ietf.org/html/rfc5322#section-3.3 */ static GstDateTime * gst_dash_demux_parse_http_head (GstDashDemuxClockDrift * clock_drift, GstFragment * download) { static const gchar *months[] = { NULL, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", NULL }; static const struct Rfc5322TimeZone timezones[] = { {"Z", 0}, {"UT", 0}, {"GMT", 0}, {"BST", 1}, {"EST", -5}, {"EDT", -4}, {"CST", -6}, {"CDT", -5}, {"MST", -7}, {"MDT", -6}, {"PST", -8}, {"PDT", -7}, {NULL, 0} }; GstDateTime *value = NULL; const GstStructure *response_headers; const gchar *http_date; const GValue *val; gint ret; const gchar *pos; gint year = -1, month = -1, day = -1, hour = -1, minute = -1, second = -1; gchar zone[6]; gchar monthstr[4]; gfloat tzoffset = 0; gboolean parsed_tz = FALSE; g_return_val_if_fail (download != NULL, NULL); g_return_val_if_fail (download->headers != NULL, NULL); val = gst_structure_get_value (download->headers, "response-headers"); if (!val) { return NULL; } response_headers = gst_value_get_structure (val); http_date = gst_structure_get_string (response_headers, "Date"); if (!http_date) { return NULL; } /* skip optional text version of day of the week */ pos = strchr (http_date, ','); if (pos) pos++; else pos = http_date; ret = sscanf (pos, "%02d %3s %04d %02d:%02d:%02d %5s", &day, monthstr, &year, &hour, &minute, &second, zone); if (ret == 7) { gchar *z = zone; for (int i = 1; months[i]; ++i) { if (g_ascii_strncasecmp (months[i], monthstr, strlen (months[i])) == 0) { month = i; break; } } for (int i = 0; timezones[i].name && !parsed_tz; ++i) { if (g_ascii_strncasecmp (timezones[i].name, z, strlen (timezones[i].name)) == 0) { tzoffset = timezones[i].tzoffset; parsed_tz = TRUE; } } if (!parsed_tz) { gint hh, mm; gboolean neg = FALSE; /* check if it is in the form +-HHMM */ if (*z == '+' || *z == '-') { if (*z == '+') ++z; else if (*z == '-') { ++z; neg = TRUE; } ret = sscanf (z, "%02d%02d", &hh, &mm); if (ret == 2) { tzoffset = hh; tzoffset += mm / 60.0; if (neg) tzoffset = -tzoffset; parsed_tz = TRUE; } } } /* Accept year in both 2 digit or 4 digit format */ if (year < 100) year += 2000; } if (month > 0 && parsed_tz) { value = gst_date_time_new (tzoffset, year, month, day, hour, minute, second); } return value; } /* The timing information is contained in the message body of the HTTP response and contains a time value formatted according to NTP timestamp format in IETF RFC 5905. 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Seconds | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Fraction | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ NTP Timestamp Format */ static GstDateTime * gst_dash_demux_parse_http_ntp (GstDashDemuxClockDrift * clock_drift, GstBuffer * buffer) { gint64 seconds; guint64 fraction; GDateTime *dt, *dt2; GstMapInfo mapinfo; /* See https://tools.ietf.org/html/rfc5905#page-12 for details of the NTP Timestamp Format */ gst_buffer_map (buffer, &mapinfo, GST_MAP_READ); if (mapinfo.size != 8) { gst_buffer_unmap (buffer, &mapinfo); return NULL; } seconds = GST_READ_UINT32_BE (mapinfo.data); fraction = GST_READ_UINT32_BE (mapinfo.data + 4); gst_buffer_unmap (buffer, &mapinfo); fraction = gst_util_uint64_scale (fraction, 1000000, G_GUINT64_CONSTANT (1) << 32); /* subtract constant to convert from 1900 based time to 1970 based time */ seconds -= NTP_TO_UNIX_EPOCH; dt = g_date_time_new_from_unix_utc (seconds); dt2 = g_date_time_add (dt, fraction); g_date_time_unref (dt); return gst_date_time_new_from_g_date_time (dt2); } /* The timing information is contained in the message body of the HTTP response and contains a time value formatted according to xs:dateTime as defined in W3C XML Schema Part 2: Datatypes specification. */ static GstDateTime * gst_dash_demux_parse_http_xsdate (GstDashDemuxClockDrift * clock_drift, GstBuffer * buffer) { GstDateTime *value = NULL; GstMapInfo mapinfo; /* the string from the server might not be zero terminated */ if (gst_buffer_map (buffer, &mapinfo, GST_MAP_READ)) { gchar *str; str = g_strndup ((const gchar *) mapinfo.data, mapinfo.size); gst_buffer_unmap (buffer, &mapinfo); value = gst_date_time_new_from_iso8601_string (str); g_free (str); } return value; } static gboolean gst_dash_demux_poll_clock_drift (GstDashDemux * demux) { GstDashDemuxClockDrift *clock_drift; GDateTime *start = NULL, *end; GstBuffer *buffer = NULL; GstDateTime *value = NULL; gboolean ret = FALSE; gint64 now; GstMPDUTCTimingType method; gchar **urls; g_return_val_if_fail (demux != NULL, FALSE); g_return_val_if_fail (demux->clock_drift != NULL, FALSE); clock_drift = demux->clock_drift; now = GST_TIME_AS_USECONDS (gst_adaptive_demux_get_monotonic_time (GST_ADAPTIVE_DEMUX_CAST (demux))); if (now < clock_drift->next_update) { /*TODO: If a fragment fails to download in adaptivedemux, it waits for a manifest reload before another attempt to fetch a fragment. Section 10.8.6 of the DVB-DASH standard states that the DASH client shall refresh the manifest and resynchronise to one of the time sources. Currently the fact that the manifest refresh follows a download failure does not make it into dashdemux. */ return TRUE; } urls = gst_mpd_client_get_utc_timing_sources (demux->client, SUPPORTED_CLOCK_FORMATS, &method); if (!urls) { return FALSE; } /* Update selected_url just in case the number of URLs in the UTCTiming element has shrunk since the last poll */ clock_drift->selected_url = clock_drift->selected_url % g_strv_length (urls); g_mutex_lock (&clock_drift->clock_lock); if (method == GST_MPD_UTCTIMING_TYPE_NTP) { value = gst_dash_demux_poll_ntp_server (clock_drift, urls); if (!value) { GST_ERROR_OBJECT (demux, "Failed to fetch time from NTP server %s", urls[clock_drift->selected_url]); g_mutex_unlock (&clock_drift->clock_lock); goto quit; } } start = gst_adaptive_demux_get_client_now_utc (GST_ADAPTIVE_DEMUX_CAST (demux)); if (!value) { GstFragment *download; gint64 range_start = 0, range_end = -1; GST_DEBUG_OBJECT (demux, "Fetching current time from %s", urls[clock_drift->selected_url]); if (method == GST_MPD_UTCTIMING_TYPE_HTTP_HEAD) { range_start = -1; } download = gst_uri_downloader_fetch_uri_with_range (GST_ADAPTIVE_DEMUX_CAST (demux)->downloader, urls[clock_drift->selected_url], NULL, TRUE, TRUE, TRUE, range_start, range_end, NULL); if (download) { if (method == GST_MPD_UTCTIMING_TYPE_HTTP_HEAD && download->headers) { value = gst_dash_demux_parse_http_head (clock_drift, download); } else { buffer = gst_fragment_get_buffer (download); } g_object_unref (download); } } g_mutex_unlock (&clock_drift->clock_lock); if (!value && !buffer) { GST_ERROR_OBJECT (demux, "Failed to fetch time from %s", urls[clock_drift->selected_url]); goto quit; } end = gst_adaptive_demux_get_client_now_utc (GST_ADAPTIVE_DEMUX_CAST (demux)); if (!value && method == GST_MPD_UTCTIMING_TYPE_HTTP_NTP) { value = gst_dash_demux_parse_http_ntp (clock_drift, buffer); } else if (!value) { /* GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE or GST_MPD_UTCTIMING_TYPE_HTTP_ISO */ value = gst_dash_demux_parse_http_xsdate (clock_drift, buffer); } if (buffer) gst_buffer_unref (buffer); if (value) { GTimeSpan download_duration = g_date_time_difference (end, start); GDateTime *client_now, *server_now; /* We don't know when the server sampled its clock, but we know it must have been before "end" and probably after "start". A reasonable estimate is to use (start+end)/2 */ client_now = g_date_time_add (start, download_duration / 2); server_now = gst_date_time_to_g_date_time (value); /* If gst_date_time_new_from_iso8601_string is given an unsupported ISO 8601 format, it can return a GstDateTime that is not valid, which causes gst_date_time_to_g_date_time to return NULL */ if (server_now) { g_mutex_lock (&clock_drift->clock_lock); clock_drift->clock_compensation = g_date_time_difference (server_now, client_now); g_mutex_unlock (&clock_drift->clock_lock); GST_DEBUG_OBJECT (demux, "Difference between client and server clocks is %lfs", ((double) clock_drift->clock_compensation) / 1000000.0); g_date_time_unref (server_now); ret = TRUE; } else { GST_ERROR_OBJECT (demux, "Failed to parse DateTime from server"); } g_date_time_unref (client_now); gst_date_time_unref (value); } else { GST_ERROR_OBJECT (demux, "Failed to parse DateTime from server"); } g_date_time_unref (end); quit: if (start) g_date_time_unref (start); /* if multiple URLs were specified, use a simple round-robin to poll each server */ g_mutex_lock (&clock_drift->clock_lock); if (method == GST_MPD_UTCTIMING_TYPE_NTP) { clock_drift->next_update = now + FAST_CLOCK_UPDATE_INTERVAL; } else { clock_drift->selected_url = (1 + clock_drift->selected_url) % g_strv_length (urls); if (ret) { clock_drift->next_update = now + SLOW_CLOCK_UPDATE_INTERVAL; } else { clock_drift->next_update = now + FAST_CLOCK_UPDATE_INTERVAL; } } g_mutex_unlock (&clock_drift->clock_lock); return ret; } static GTimeSpan gst_dash_demux_get_clock_compensation (GstDashDemux * demux) { GTimeSpan rv = 0; if (demux->clock_drift) { g_mutex_lock (&demux->clock_drift->clock_lock); rv = demux->clock_drift->clock_compensation; g_mutex_unlock (&demux->clock_drift->clock_lock); } GST_LOG_OBJECT (demux, "Clock drift %" GST_STIME_FORMAT, GST_STIME_ARGS (rv)); return rv; } static GDateTime * gst_dash_demux_get_server_now_utc (GstDashDemux * demux) { GDateTime *client_now; GDateTime *server_now; client_now = gst_adaptive_demux_get_client_now_utc (GST_ADAPTIVE_DEMUX_CAST (demux)); server_now = g_date_time_add (client_now, gst_dash_demux_get_clock_compensation (demux)); g_date_time_unref (client_now); return server_now; }