#include <string.h>
#include <inttypes.h>
+#include <gio/gio.h>
#include <gst/base/gsttypefindhelper.h>
#include <gst/tag/tag.h>
+#include <gst/net/gstnet.h>
#include "gst/gst-i18n-plugin.h"
#include "gstdashdemux.h"
#include "gstdash_debug.h"
#define DEFAULT_BANDWIDTH_USAGE 0.8 /* 0 to 1 */
#define DEFAULT_MAX_BITRATE 24000000 /* in bit/s */
+/* 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_XSDATE | GST_MPD_UTCTIMING_TYPE_HTTP_ISO | GST_MPD_UTCTIMING_TYPE_HTTP_NTP)
+#define NTP_TO_UNIX_EPOCH 2208988800LL /* 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;
+ GCond clock_cond; /* used for waiting until got_clock==TRUE */
+ /* @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;
+ gboolean got_clock; /* indicates time source has returned a valid clock at least once */
+ GstClock *ntp_clock;
+};
+
/* GObject */
static void gst_dash_demux_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
GstActiveStream * stream);
static GstPad *gst_dash_demux_create_pad (GstDashDemux * demux,
GstActiveStream * stream);
+static GstDashDemuxClockDrift *gst_dash_demux_clock_drift_new (void);
+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)]))
g_mutex_clear (&demux->client_lock);
+ gst_dash_demux_clock_drift_free (demux->clock_drift);
+ demux->clock_drift = NULL;
G_OBJECT_CLASS (parent_class)->dispose (obj);
}
gint64 * stop)
{
GstDashDemux *self = GST_DASH_DEMUX (demux);
- GDateTime *now = g_date_time_new_now_utc ();
+ GDateTime *now = gst_dash_demux_get_server_now_utc (self);
GDateTime *mstart =
gst_date_time_to_g_date_time (self->client->
mpd_node->availabilityStartTime);
* non-live */
period_idx = 0;
if (gst_mpd_client_is_live (dashdemux->client)) {
-
+ GDateTime *g_now;
+ 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 ();
+ gst_dash_demux_poll_clock_drift (dashdemux);
+ }
+ }
/* get period index for period encompassing the current time */
- now = gst_date_time_new_now_utc ();
+ 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_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 ();
demux->n_audio_streams = 0;
gst_dash_demux_get_manifest_update_interval (GstAdaptiveDemux * demux)
{
GstDashDemux *dashdemux = GST_DASH_DEMUX_CAST (demux);
- return dashdemux->client->mpd_node->minimumUpdatePeriod * 1000;
+ return MIN (dashdemux->client->mpd_node->minimumUpdatePeriod * 1000,
+ SLOW_CLOCK_UPDATE_INTERVAL);
}
static GstFlowReturn
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
diff = gst_mpd_client_calculate_time_difference (cur_time, seg_end_time);
gst_date_time_unref (seg_end_time);
gst_date_time_unref (cur_time);
- return diff;
+ /* 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);
}
return 0;
}
gst_isoff_sidx_parser_clear (&dash_stream->sidx_parser);
}
+
+static GstDashDemuxClockDrift *
+gst_dash_demux_clock_drift_new (void)
+{
+ GstDashDemuxClockDrift *clock_drift;
+
+ clock_drift = g_slice_new0 (GstDashDemuxClockDrift);
+ g_mutex_init (&clock_drift->clock_lock);
+ g_cond_init (&clock_drift->clock_cond);
+ clock_drift->next_update = g_get_monotonic_time ();
+ 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_cond_clear (&clock_drift->clock_cond);
+ g_mutex_unlock (&clock_drift->clock_lock);
+ g_mutex_clear (&clock_drift->clock_lock);
+ g_slice_free (GstDashDemuxClockDrift, clock_drift);
+ }
+}
+
+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->message);
+ if (inet_addrs)
+ g_resolver_free_addresses (inet_addrs);
+ 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);
+}
+
+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);
+}
+
+static GstDateTime *
+gst_dash_demux_parse_http_xsdate (GstDashDemuxClockDrift * clock_drift,
+ GstBuffer * buffer)
+{
+ GstDateTime *value;
+ GstMapInfo mapinfo;
+
+ /* the string from the server might not be zero terminated */
+ gst_buffer_resize (buffer, 0, gst_buffer_get_size (buffer) + 1);
+ gst_buffer_map (buffer, &mapinfo, GST_MAP_READ | GST_MAP_WRITE);
+ mapinfo.data[mapinfo.size - 1] = '\0';
+ value = gst_date_time_new_from_iso8601_string ((const gchar *) mapinfo.data);
+ gst_buffer_unmap (buffer, &mapinfo);
+ 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 = g_get_monotonic_time ();
+ 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 = g_date_time_new_now_utc ();
+ if (!value) {
+ GstFragment *download;
+ GST_DEBUG_OBJECT (demux, "Fetching current time from %s",
+ urls[clock_drift->selected_url]);
+ download =
+ gst_uri_downloader_fetch_uri (GST_ADAPTIVE_DEMUX_CAST
+ (demux)->downloader, urls[clock_drift->selected_url], NULL, TRUE, TRUE,
+ TRUE, NULL);
+ 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 = g_date_time_new_now_utc ();
+ 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);
+ clock_drift->got_clock = TRUE;
+ g_cond_broadcast (&clock_drift->clock_cond);
+ 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 = g_date_time_new_now_utc ();
+ GDateTime *server_now = g_date_time_add (client_now,
+ gst_dash_demux_get_clock_compensation (demux));
+ g_date_time_unref (client_now);
+ return server_now;
+}
static void gst_mpdparser_parse_metrics_node (GList ** list, xmlNode * a_node);
static void gst_mpdparser_parse_root_node (GstMPDNode ** pointer,
xmlNode * a_node);
+static void gst_mpdparser_parse_utctiming_node (GList ** list,
+ xmlNode * a_node);
/* Helper functions */
static gint convert_to_millisecs (gint decimals, gint pos);
descriptor_type);
static void gst_mpdparser_free_content_component_node (GstContentComponentNode *
content_component_node);
+static void gst_mpdparser_free_utctiming_node (GstUTCTimingNode * timing_type);
static void gst_mpdparser_free_stream_period (GstStreamPeriod * stream_period);
static void gst_mpdparser_free_media_segment (GstMediaSegment * media_segment);
static void gst_mpdparser_free_active_stream (GstActiveStream * active_stream);
+struct GstMpdParserUtcTimingMethod
+{
+ const gchar *name;
+ GstMPDUTCTimingType method;
+};
+
+static const struct GstMpdParserUtcTimingMethod
+ gst_mpdparser_utc_timing_methods[] = {
+ {"urn:mpeg:dash:utc:ntp:2014", GST_MPD_UTCTIMING_TYPE_NTP},
+ {"urn:mpeg:dash:utc:sntp:2014", GST_MPD_UTCTIMING_TYPE_SNTP},
+ {"urn:mpeg:dash:utc:http-head:2014", GST_MPD_UTCTIMING_TYPE_HTTP_HEAD},
+ {"urn:mpeg:dash:utc:http-xsdate:2014", GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE},
+ {"urn:mpeg:dash:utc:http-iso:2014", GST_MPD_UTCTIMING_TYPE_HTTP_ISO},
+ {"urn:mpeg:dash:utc:http-ntp:2014", GST_MPD_UTCTIMING_TYPE_HTTP_NTP},
+ {"urn:mpeg:dash:utc:direct:2014", GST_MPD_UTCTIMING_TYPE_DIRECT},
+ /*
+ * Early working drafts used the :2012 namespace and this namespace is
+ * used by some DASH packagers. To work-around these packagers, we also
+ * accept the early draft scheme names.
+ */
+ {"urn:mpeg:dash:utc:ntp:2012", GST_MPD_UTCTIMING_TYPE_NTP},
+ {"urn:mpeg:dash:utc:sntp:2012", GST_MPD_UTCTIMING_TYPE_SNTP},
+ {"urn:mpeg:dash:utc:http-head:2012", GST_MPD_UTCTIMING_TYPE_HTTP_HEAD},
+ {"urn:mpeg:dash:utc:http-xsdate:2012", GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE},
+ {"urn:mpeg:dash:utc:http-iso:2012", GST_MPD_UTCTIMING_TYPE_HTTP_ISO},
+ {"urn:mpeg:dash:utc:http-ntp:2012", GST_MPD_UTCTIMING_TYPE_HTTP_NTP},
+ {"urn:mpeg:dash:utc:direct:2012", GST_MPD_UTCTIMING_TYPE_DIRECT},
+ {NULL, 0}
+};
+
/* functions to parse node namespaces, content and properties */
static gboolean
gst_mpdparser_get_xml_prop_string (xmlNode * a_node,
}
}
+/* The UTCTiming element is defined in
+ * ISO/IEC 23009-1:2014/PDAM 1 "Information technology — Dynamic adaptive streaming over HTTP (DASH) — Part 1: Media presentation description and segment formats / Amendment 1: High Profile and Availability Time Synchronization"
+ */
+static void
+gst_mpdparser_parse_utctiming_node (GList ** list, xmlNode * a_node)
+{
+ GstUTCTimingNode *new_timing;
+ gchar *method = NULL;
+ gchar *value = NULL;
+
+ new_timing = g_slice_new0 (GstUTCTimingNode);
+
+ GST_LOG ("attributes of UTCTiming node:");
+ if (gst_mpdparser_get_xml_prop_string (a_node, "schemeIdUri", &method)) {
+ for (int i = 0; gst_mpdparser_utc_timing_methods[i].name; ++i) {
+ if (g_ascii_strncasecmp (gst_mpdparser_utc_timing_methods[i].name,
+ method, strlen (gst_mpdparser_utc_timing_methods[i].name)) == 0) {
+ new_timing->method = gst_mpdparser_utc_timing_methods[i].method;
+ break;
+ }
+ }
+ xmlFree (method);
+ }
+ if (gst_mpdparser_get_xml_prop_string (a_node, "value", &value)) {
+ int max_tokens = 0;
+ if (GST_MPD_UTCTIMING_TYPE_DIRECT == new_timing->method) {
+ /* The GST_MPD_UTCTIMING_TYPE_DIRECT method is a special case
+ * that is not a space separated list.
+ */
+ max_tokens = 1;
+ }
+ new_timing->urls = g_strsplit (value, " ", max_tokens);
+ xmlFree (value);
+ *list = g_list_append (*list, new_timing);
+ }
+}
+
static void
gst_mpdparser_parse_root_node (GstMPDNode ** pointer, xmlNode * a_node)
{
gst_mpdparser_parse_location_node (&new_mpd->Locations, cur_node);
} else if (xmlStrcmp (cur_node->name, (xmlChar *) "Metrics") == 0) {
gst_mpdparser_parse_metrics_node (&new_mpd->Metrics, cur_node);
+ } else if (xmlStrcmp (cur_node->name, (xmlChar *) "UTCTiming") == 0) {
+ gst_mpdparser_parse_utctiming_node (&new_mpd->UTCTiming, cur_node);
}
}
}
(GDestroyNotify) gst_mpdparser_free_period_node);
g_list_free_full (mpd_node->Metrics,
(GDestroyNotify) gst_mpdparser_free_metrics_node);
+ g_list_free_full (mpd_node->UTCTiming,
+ (GDestroyNotify) gst_mpdparser_free_utctiming_node);
g_slice_free (GstMPDNode, mpd_node);
}
}
}
static void
+gst_mpdparser_free_utctiming_node (GstUTCTimingNode * timing_type)
+{
+ if (timing_type) {
+ if (timing_type->urls)
+ g_strfreev (timing_type->urls);
+ g_slice_free (GstUTCTimingNode, timing_type);
+ }
+}
+
+static void
gst_mpdparser_free_stream_period (GstStreamPeriod * stream_period)
{
if (stream_period) {
return 0;
}
+/**
+ * gst_mpd_client_get_utc_timing_sources:
+ * @client: #GstMpdClient to check for UTCTiming elements
+ * @methods: A bit mask of #GstMPDUTCTimingType that specifies the methods
+ * to search for.
+ * @selected_method: (nullable): The selected method
+ * Returns: (transfer none): A NULL terminated array of URLs of servers
+ * that use @selected_method to provide a realtime clock.
+ *
+ * Searches the UTCTiming elements found in the manifest for an element
+ * that uses one of the UTC timing methods specified in @selected_method.
+ * If multiple UTCTiming elements are present that support one of the
+ * methods specified in @selected_method, the first one is returned.
+ *
+ * Since: 1.6
+ */
+gchar **
+gst_mpd_client_get_utc_timing_sources (GstMpdClient * client,
+ guint methods, GstMPDUTCTimingType * selected_method)
+{
+ GList *list;
+
+ g_return_val_if_fail (client != NULL, NULL);
+ g_return_val_if_fail (client->mpd_node != NULL, NULL);
+ for (list = g_list_first (client->mpd_node->UTCTiming); list;
+ list = g_list_next (list)) {
+ const GstUTCTimingNode *node = (const GstUTCTimingNode *) list->data;
+ if (node->method & methods) {
+ if (selected_method) {
+ *selected_method = node->method;
+ }
+ return node->urls;
+ }
+ }
+ return NULL;
+}
+
gboolean
gst_mpd_client_get_next_fragment (GstMpdClient * client,
guint indexStream, GstMediaFragmentInfo * fragment)