From 95c705ae8f48acb4504ac168b4a8c16d77cae8da Mon Sep 17 00:00:00 2001 From: Alex Ashley Date: Wed, 15 Jul 2015 11:56:13 +0100 Subject: [PATCH] dashdemux: add support for UTCTiming elements for clock drift compensation Unless the DASH client can compensate for the difference between its clock and the clock used by the server, the client might request fragments that either not yet on the server or fragments that have already been expired from the server. This is an issue because these requests can propagate all the way back to the origin ISO/IEC 23009-1:2014/Amd 1 [PDAM1] defines a new UTCTiming element to allow a DASH client to track the clock used by the server generating the DASH stream. Multiple UTCTiming elements might be present, to indicate support for multiple methods of UTC time gathering. Each element can contain a white space separated list of URLs that can be contacted to discover the UTC time from the server's perspective. This commit provides parsing of UTCTiming elements, unit tests of this parsing and a function to poll a time server. This function supports the following methods: urn:mpeg:dash:utc:ntp:2014 urn:mpeg:dash:utc:http-xsdate:2014 urn:mpeg:dash:utc:http-iso:2014 urn:mpeg:dash:utc:http-ntp:2014 The manifest update task is used to poll the clock time server, to save having to create a new thread. When choosing the starting fragment number and when waiting for a fragment to become available, the difference between the server's idea of UTC and the client's idea of UTC is taken into account. For example, if the server's time is behind the client's idea of UTC, we wait for longer before requesting a fragment [PDAM1]: http://www.iso.org/iso/home/store/catalogue_tc/catalogue_detail.htm?csnumber=66068 dashdemux: support NTP time servers in UTCTiming elements Use the gst_ntp_clock to support the use of an NTP server. https://bugzilla.gnome.org/show_bug.cgi?id=752413 --- ext/dash/Makefile.am | 1 + ext/dash/gstdashdemux.c | 342 +++++++++++++++++++++++++++++++++++++++- ext/dash/gstdashdemux.h | 3 + ext/dash/gstmpdparser.c | 121 ++++++++++++++ ext/dash/gstmpdparser.h | 22 +++ tests/check/elements/dash_mpd.c | 57 +++++++ 6 files changed, 541 insertions(+), 5 deletions(-) diff --git a/ext/dash/Makefile.am b/ext/dash/Makefile.am index f4e7290..deae7ac 100644 --- a/ext/dash/Makefile.am +++ b/ext/dash/Makefile.am @@ -27,6 +27,7 @@ libgstdashdemux_la_LIBADD = \ -lgsttag-$(GST_API_VERSION) \ $(GST_BASE_LIBS) \ $(GST_LIBS) \ + -lgstnet-$(GST_API_VERSION) \ $(LIBXML2_LIBS) libgstdashdemux_la_LDFLAGS = $(GST_PLUGIN_LDFLAGS) libgstdashdemux_la_LIBTOOLFLAGS = --tag=disable-static diff --git a/ext/dash/gstdashdemux.c b/ext/dash/gstdashdemux.c index cfa8ffa..bbda84d 100644 --- a/ext/dash/gstdashdemux.c +++ b/ext/dash/gstdashdemux.c @@ -146,8 +146,10 @@ #include #include +#include #include #include +#include #include "gst/gst-i18n-plugin.h" #include "gstdashdemux.h" #include "gstdash_debug.h" @@ -187,6 +189,25 @@ enum #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); @@ -236,6 +257,11 @@ 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 (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)])) @@ -264,6 +290,8 @@ gst_dash_demux_dispose (GObject * obj) 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); } @@ -272,7 +300,7 @@ gst_dash_demux_get_live_seek_range (GstAdaptiveDemux * demux, gint64 * start, 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); @@ -606,9 +634,21 @@ gst_dash_demux_setup_streams (GstAdaptiveDemux * demux) * 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); @@ -751,6 +791,8 @@ gst_dash_demux_reset (GstAdaptiveDemux * ademux) 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; @@ -1186,7 +1228,8 @@ static gint64 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 @@ -1282,6 +1325,9 @@ gst_dash_demux_update_manifest_data (GstAdaptiveDemux * demux, 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 @@ -1317,7 +1363,10 @@ gst_dash_demux_stream_get_fragment_waiting_time (GstAdaptiveDemuxStream * 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; } @@ -1480,3 +1529,286 @@ gst_dash_demux_stream_free (GstAdaptiveDemuxStream * stream) 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; +} diff --git a/ext/dash/gstdashdemux.h b/ext/dash/gstdashdemux.h index c945a19..ca2af31 100644 --- a/ext/dash/gstdashdemux.h +++ b/ext/dash/gstdashdemux.h @@ -52,6 +52,7 @@ G_BEGIN_DECLS #define GST_DASH_DEMUX_CAST(obj) \ ((GstDashDemux *)obj) +typedef struct _GstDashDemuxClockDrift GstDashDemuxClockDrift; typedef struct _GstDashDemuxStream GstDashDemuxStream; typedef struct _GstDashDemux GstDashDemux; typedef struct _GstDashDemuxClass GstDashDemuxClass; @@ -87,6 +88,8 @@ struct _GstDashDemux GstMpdClient *client; /* MPD client */ GMutex client_lock; + GstDashDemuxClockDrift *clock_drift; + gboolean end_of_period; gboolean end_of_manifest; diff --git a/ext/dash/gstmpdparser.c b/ext/dash/gstmpdparser.c index c588142..f500c1f 100644 --- a/ext/dash/gstmpdparser.c +++ b/ext/dash/gstmpdparser.c @@ -112,6 +112,8 @@ static void gst_mpdparser_parse_metrics_range_node (GList ** list, 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); @@ -200,10 +202,41 @@ static void gst_mpdparser_free_descriptor_type_node (GstDescriptorType * 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, @@ -1766,6 +1799,43 @@ gst_mpdparser_parse_metrics_node (GList ** list, 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) { @@ -1820,6 +1890,8 @@ 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); } } } @@ -2057,6 +2129,8 @@ gst_mpdparser_free_mpd_node (GstMPDNode * mpd_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); } } @@ -2395,6 +2469,16 @@ gst_mpdparser_free_content_component_node (GstContentComponentNode * } 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) { @@ -3663,6 +3747,43 @@ gst_mpd_parser_get_stream_presentation_offset (GstMpdClient * client, 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) diff --git a/ext/dash/gstmpdparser.h b/ext/dash/gstmpdparser.h index 36e4590..b31b7e6 100644 --- a/ext/dash/gstmpdparser.h +++ b/ext/dash/gstmpdparser.h @@ -56,6 +56,7 @@ typedef struct _GstSubsetNode GstSubsetNode; typedef struct _GstProgramInformationNode GstProgramInformationNode; typedef struct _GstMetricsRangeNode GstMetricsRangeNode; typedef struct _GstMetricsNode GstMetricsNode; +typedef struct _GstUTCTimingNode GstUTCTimingNode; typedef struct _GstSNode GstSNode; typedef struct _GstSegmentTimelineNode GstSegmentTimelineNode; typedef struct _GstSegmentBaseType GstSegmentBaseType; @@ -90,6 +91,18 @@ typedef enum GST_SAP_TYPE_6 } GstSAPType; +typedef enum +{ + GST_MPD_UTCTIMING_TYPE_UNKNOWN = 0x00, + GST_MPD_UTCTIMING_TYPE_NTP = 0x01, + GST_MPD_UTCTIMING_TYPE_SNTP = 0x02, + GST_MPD_UTCTIMING_TYPE_HTTP_HEAD = 0x04, + GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE = 0x08, + GST_MPD_UTCTIMING_TYPE_HTTP_ISO = 0x10, + GST_MPD_UTCTIMING_TYPE_HTTP_NTP = 0x20, + GST_MPD_UTCTIMING_TYPE_DIRECT = 0x40 +} GstMPDUTCTimingType; + struct _GstBaseURL { gchar *baseURL; @@ -362,6 +375,12 @@ struct _GstMetricsNode GList *Reportings; }; +struct _GstUTCTimingNode { + GstMPDUTCTimingType method; + /* NULL terminated array of strings */ + gchar **urls; +}; + struct _GstMPDNode { gchar *default_namespace; @@ -390,6 +409,8 @@ struct _GstMPDNode GList *Periods; /* list of Metrics nodes */ GList *Metrics; + /* list of GstUTCTimingNode nodes */ + GList *UTCTiming; }; /** @@ -505,6 +526,7 @@ gboolean gst_mpd_client_stream_seek (GstMpdClient * client, GstActiveStream * st gboolean gst_mpd_client_seek_to_time (GstMpdClient * client, GDateTime * time); gint gst_mpd_client_check_time_position (GstMpdClient * client, GstActiveStream * stream, GstClockTime ts, gint64 * diff); GstClockTime gst_mpd_parser_get_stream_presentation_offset (GstMpdClient *client, guint stream_idx); +gchar** gst_mpd_client_get_utc_timing_sources (GstMpdClient *client, guint methods, GstMPDUTCTimingType *selected_method); /* Period selection */ guint gst_mpd_client_get_period_index_at_time (GstMpdClient * client, GstDateTime * time); diff --git a/tests/check/elements/dash_mpd.c b/tests/check/elements/dash_mpd.c index b1f7762..ee5b6cb 100644 --- a/tests/check/elements/dash_mpd.c +++ b/tests/check/elements/dash_mpd.c @@ -2268,6 +2268,62 @@ GST_START_TEST (dash_mpdparser_period_subset) GST_END_TEST; /* + * Test parsing UTCTiming elements + * + */ +GST_START_TEST (dash_mpdparser_utctiming) +{ + const gchar *xml = + "" + "" + "" + "" + "" + ""; + gboolean ret; + GstMpdClient *mpdclient = gst_mpd_client_new (); + GstMPDUTCTimingType selected_method; + gchar **urls; + + ret = gst_mpd_parse (mpdclient, xml, (gint) strlen (xml)); + + assert_equals_int (ret, TRUE); + fail_if (mpdclient->mpd_node == NULL); + fail_if (mpdclient->mpd_node->UTCTiming == NULL); + assert_equals_int (g_list_length (mpdclient->mpd_node->UTCTiming), 3); + urls = + gst_mpd_client_get_utc_timing_sources (mpdclient, + GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE, &selected_method); + fail_if (urls == NULL); + assert_equals_int (selected_method, GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE); + assert_equals_int (g_strv_length (urls), 2); + assert_equals_string (urls[0], "http://time.akamai.com/?iso"); + assert_equals_string (urls[1], "http://example.time/xsdate"); + urls = + gst_mpd_client_get_utc_timing_sources (mpdclient, + GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE | GST_MPD_UTCTIMING_TYPE_HTTP_ISO, + &selected_method); + fail_if (urls == NULL); + assert_equals_int (selected_method, GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE); + urls = + gst_mpd_client_get_utc_timing_sources (mpdclient, + GST_MPD_UTCTIMING_TYPE_DIRECT, NULL); + fail_if (urls == NULL); + assert_equals_int (g_strv_length (urls), 1); + assert_equals_string (urls[0], "2002-05-30T09:30:10Z "); + urls = + gst_mpd_client_get_utc_timing_sources (mpdclient, + GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE | GST_MPD_UTCTIMING_TYPE_DIRECT, + &selected_method); + fail_if (urls == NULL); + assert_equals_int (selected_method, GST_MPD_UTCTIMING_TYPE_HTTP_XSDATE); + gst_mpd_client_free (mpdclient); +} + +GST_END_TEST; + +/* * Test parsing the type property: value "dynamic" * */ @@ -4333,6 +4389,7 @@ dash_suite (void) tcase_add_test (tc_simpleMPD, dash_mpdparser_period_adaptationSet_representation_segmentTemplate); tcase_add_test (tc_simpleMPD, dash_mpdparser_period_subset); + tcase_add_test (tc_simpleMPD, dash_mpdparser_utctiming); /* tests checking other possible values for attributes */ tcase_add_test (tc_simpleMPD, dash_mpdparser_type_dynamic); -- 2.7.4