dashdemux: add support for HTTP HEAD method of time sync
authorAlex Ashley <bugzilla@ashley-family.net>
Fri, 14 Aug 2015 08:44:24 +0000 (09:44 +0100)
committerThiago Santos <thiagoss@osg.samsung.com>
Fri, 14 Aug 2015 09:47:21 +0000 (06:47 -0300)
The urn:mpeg:dash:utc:http-head:2014 method of time synchronisation
uses an HTTP HEAD request to a specified URL and then parses the
Date: HTTP response header.

This commit adds support to dashdemux for this method of time
synchronisation by making a HEAD request and then parsing the Date:
response.

This commit adds support to gstfragment to return the HTTP headers
and to uridownloader to support HEAD requests. To avoid creating a
new API, the RANGE get function is re-used (abused?) with start=-1
and end=-1 to indicate a HEAD request.

https://bugzilla.gnome.org/show_bug.cgi?id=752413

ext/dash/gstdashdemux.c
gst-libs/gst/uridownloader/gstfragment.c
gst-libs/gst/uridownloader/gstfragment.h
gst-libs/gst/uridownloader/gsturidownloader.c

index 1e7dab6..06df497 100644 (file)
 #endif
 
 #include <string.h>
+#include <stdio.h>
 #include <inttypes.h>
 #include <gio/gio.h>
 #include <gst/base/gsttypefindhelper.h>
@@ -192,7 +193,7 @@ enum
 /* 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 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
@@ -1631,6 +1632,121 @@ gst_dash_demux_poll_ntp_server (GstDashDemuxClockDrift * clock_drift,
   return gst_date_time_new_from_g_date_time (dt2);
 }
 
+struct Rfc822TimeZone
+{
+  const gchar *name;
+  gfloat tzoffset;
+};
+
+/*
+ Parse an RFC822 (section 5) date-time from the Date: field in the
+ HTTP response. 
+ See https://tools.ietf.org/html/rfc822#section-5
+*/
+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 Rfc822TimeZone 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;
+      }
+    }
+    while (*z == ' ') {
+      ++z;
+    }
+    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;
+        }
+      }
+    }
+  }
+  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
@@ -1744,14 +1860,24 @@ gst_dash_demux_poll_clock_drift (GstDashDemux * demux)
   start = g_date_time_new_now_utc ();
   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 (GST_ADAPTIVE_DEMUX_CAST
+        gst_uri_downloader_fetch_uri_with_range (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);
+        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) {
index 3f9552b..6d41593 100644 (file)
@@ -167,6 +167,7 @@ gst_fragment_init (GstFragment * fragment)
   fragment->name = g_strdup ("");
   fragment->completed = FALSE;
   fragment->discontinuous = FALSE;
+  fragment->headers = NULL;
 }
 
 GstFragment *
@@ -183,6 +184,8 @@ gst_fragment_finalize (GObject * gobject)
   g_free (fragment->uri);
   g_free (fragment->redirect_uri);
   g_free (fragment->name);
+  if (fragment->headers)
+    gst_structure_free (fragment->headers);
   g_mutex_clear (&fragment->priv->lock);
 
   G_OBJECT_CLASS (gst_fragment_parent_class)->finalize (gobject);
index bdf04f3..a8e0a54 100644 (file)
@@ -44,6 +44,8 @@ struct _GstFragment
   gchar * uri;                  /* URI of the fragment */
   gchar * redirect_uri;         /* Redirect target if any */
   gboolean redirect_permanent;  /* If the redirect is permanent */
+  gint64 range_start;
+  gint64 range_end;
 
   gchar * name;                 /* Name of the fragment */
   gboolean completed;           /* Whether the fragment is complete or not */
@@ -53,6 +55,7 @@ struct _GstFragment
   guint64 stop_time;            /* Stop time of the fragment */
   gboolean index;               /* Index of the fragment */
   gboolean discontinuous;       /* Whether this fragment is discontinuous or not */
+  GstStructure *headers;        /* HTTP request/response headers */
 
   GstFragmentPrivate *priv;
 };
index 3d5a215..72e5e79 100644 (file)
@@ -178,6 +178,20 @@ gst_uri_downloader_sink_event (GstPad * pad, GstObject * parent,
       gst_event_unref (event);
       break;
     }
+    case GST_EVENT_CUSTOM_DOWNSTREAM_STICKY:{
+      const GstStructure *str;
+      str = gst_event_get_structure (event);
+      if (gst_structure_has_name (str, "http-headers")) {
+        GST_OBJECT_LOCK (downloader);
+        if (downloader->priv->download != NULL) {
+          if (downloader->priv->download->headers)
+            gst_structure_free (downloader->priv->download->headers);
+          downloader->priv->download->headers = gst_structure_copy (str);
+        }
+        GST_OBJECT_UNLOCK (downloader);
+      }
+    }
+      /* falls through */
     default:
       ret = gst_pad_event_default (pad, parent, event);
       break;
@@ -428,6 +442,23 @@ gst_uri_downloader_set_uri (GstUriDownloader * downloader, const gchar * uri,
   return TRUE;
 }
 
+static gboolean
+gst_uri_downloader_set_method (GstUriDownloader * downloader,
+    const gchar * method)
+{
+  GObjectClass *gobject_class;
+
+  if (!downloader->priv->urisrc)
+    return FALSE;
+
+  gobject_class = G_OBJECT_GET_CLASS (downloader->priv->urisrc);
+  if (g_object_class_find_property (gobject_class, "method")) {
+    g_object_set (downloader->priv->urisrc, "method", method, NULL);
+    return TRUE;
+  }
+  return FALSE;
+}
+
 GstFragment *
 gst_uri_downloader_fetch_uri (GstUriDownloader * downloader,
     const gchar * uri, const gchar * referer, gboolean compress,
@@ -477,6 +508,8 @@ gst_uri_downloader_fetch_uri_with_range (GstUriDownloader *
   if (downloader->priv->download)
     g_object_unref (downloader->priv->download);
   downloader->priv->download = gst_fragment_new ();
+  downloader->priv->download->range_start = range_start;
+  downloader->priv->download->range_end = range_end;
   GST_OBJECT_UNLOCK (downloader);
   ret = gst_element_set_state (downloader->priv->urisrc, GST_STATE_READY);
   GST_OBJECT_LOCK (downloader);
@@ -490,9 +523,16 @@ gst_uri_downloader_fetch_uri_with_range (GstUriDownloader *
     goto quit;
   }
 
-  if (!gst_uri_downloader_set_range (downloader, range_start, range_end)) {
-    GST_WARNING_OBJECT (downloader, "Failed to set range");
-    goto quit;
+  if (range_start < 0 && range_end < 0) {
+    if (!gst_uri_downloader_set_method (downloader, "HEAD")) {
+      GST_WARNING_OBJECT (downloader, "Failed to set HTTP method");
+      goto quit;
+    }
+  } else {
+    if (!gst_uri_downloader_set_range (downloader, range_start, range_end)) {
+      GST_WARNING_OBJECT (downloader, "Failed to set range");
+      goto quit;
+    }
   }
 
   GST_OBJECT_UNLOCK (downloader);
@@ -531,9 +571,13 @@ gst_uri_downloader_fetch_uri_with_range (GstUriDownloader *
   download = downloader->priv->download;
   downloader->priv->download = NULL;
   if (!downloader->priv->got_buffer) {
-    g_object_unref (download);
-    download = NULL;
-    GST_ERROR_OBJECT (downloader, "Didn't retrieve a buffer before EOS");
+    if (download->range_start < 0 && download->range_end < 0) {
+      /* HEAD request, so we don't expect a response */
+    } else {
+      g_object_unref (download);
+      download = NULL;
+      GST_ERROR_OBJECT (downloader, "Didn't retrieve a buffer before EOS");
+    }
   }
 
   if (download != NULL)