diff --git a/ext/ogg/gstoggdemux.c b/ext/ogg/gstoggdemux.c
index b6e78b6..4a10e1a 100644
--- a/ext/ogg/gstoggdemux.c
+++ b/ext/ogg/gstoggdemux.c
@@ -50,9 +50,30 @@ GST_ELEMENT_DETAILS ("Ogg demuxer",
     "demux ogg streams (info about ogg: http://xiph.org)",
     "Wim Taymans <wim@fluendo.com>");
 
+typedef struct _GstOggIndexKeypoint GstOggIndexKeypoint;
+typedef struct _GstOggIndexSearchState GstOggIndexSearchState;
+
+struct _GstOggIndexKeypoint
+{
+  gint64 prestime;
+  guint64 page_offset;
+  guint32 page_checksum;
+};
+
+struct _GstOggIndexSearchState
+{
+  gint64 ideal_prestime;
+  GstOggIndexKeypoint *nearest_keypoint;
+};
+
 #define CHUNKSIZE (8500)        /* this is out of vorbisfile */
-#define SKELETON_FISHEAD_SIZE 64
+#define SKELETON_FISHEAD_MIN_SIZE 64
+#define SKELETON_FISHEAD_3_2_MIN_SIZE 112
 #define SKELETON_FISBONE_MIN_SIZE 52
+#define SKELETON_INDEX_HEADER_SIZE 26
+#define SKELETON_INDEX_KEYPOINT_SIZE 20
+#define SKELETON_INDEX_MAX_KEYPOINTS ((G_MAXUINT64 - \
+    SKELETON_INDEX_HEADER_SIZE) / SKELETON_INDEX_KEYPOINT_SIZE)
 
 #define GST_FLOW_LIMIT GST_FLOW_CUSTOM_ERROR
 
@@ -63,6 +84,58 @@ GST_DEBUG_CATEGORY (gst_ogg_demux_debug);
 GST_DEBUG_CATEGORY (gst_ogg_demux_setup_debug);
 #define GST_CAT_DEFAULT gst_ogg_demux_debug
 
+static gint
+gst_ogg_index_keypoint_compare (gconstpointer a, gconstpointer b)
+{
+  GstOggIndexKeypoint *keypoint1 = (GstOggIndexKeypoint *)a;
+  GstOggIndexKeypoint *keypoint2 = (GstOggIndexKeypoint *)b;
+
+  if (keypoint1->prestime == keypoint2->prestime)
+    return 0;
+  else if (keypoint1->prestime < keypoint2->prestime)
+    return -1;
+  else
+    return 1;
+}
+
+static gint
+gst_ogg_index_keypoint_search_compare (gconstpointer a, gconstpointer data)
+{
+  GstOggIndexSearchState *search_state = (GstOggIndexSearchState *)data;
+  GstOggIndexKeypoint *keypoint = (GstOggIndexKeypoint *)a;
+  GstOggIndexKeypoint *nearest_keypoint = search_state->nearest_keypoint;
+
+  if (nearest_keypoint == NULL ||
+      ABS(keypoint->prestime - search_state->ideal_prestime) <
+      ABS(nearest_keypoint->prestime - search_state->ideal_prestime))
+  {
+    search_state->nearest_keypoint = keypoint;
+  }
+
+  if (search_state->ideal_prestime == keypoint->prestime)
+    return 0;
+  else if (search_state->ideal_prestime < keypoint->prestime)
+    return -1;
+  else
+    return 1;
+}
+
+static GstOggIndexKeypoint *
+gst_ogg_index_keypoint_find_nearest (GstOggPad *pad, gint64 ideal_prestime)
+{
+  GstOggIndexSearchState search_state;
+
+  search_state.ideal_prestime = ideal_prestime;
+  search_state.nearest_keypoint = NULL;
+
+  if (pad->map.index_keypoints_tree) {
+    g_tree_search (pad->map.index_keypoints_tree,
+        gst_ogg_index_keypoint_search_compare, &search_state);
+  }
+
+  return search_state.nearest_keypoint;
+}
+
 static ogg_page *
 gst_ogg_page_copy (ogg_page * page)
 {
@@ -93,6 +166,10 @@ static void gst_ogg_chain_mark_discont (GstOggChain * chain);
 
 static gboolean gst_ogg_demux_perform_seek (GstOggDemux * ogg,
     GstEvent * event);
+static gboolean gst_ogg_demux_perform_seek_pull (GstOggDemux * ogg,
+    GstEvent * event);
+static gboolean gst_ogg_demux_perform_seek_push (GstOggDemux * ogg,
+    GstEvent * event);
 static gboolean gst_ogg_demux_receive_event (GstElement * element,
     GstEvent * event);
 
@@ -150,6 +227,8 @@ gst_ogg_pad_init (GstOggPad * pad)
   pad->continued = NULL;
   pad->map.headers = NULL;
   pad->map.queued = NULL;
+  pad->map.index_keypoints_array = NULL;
+  pad->map.index_keypoints_tree = NULL;
 }
 
 static void
@@ -167,6 +246,11 @@ gst_ogg_pad_dispose (GObject * object)
   g_list_free (pad->map.queued);
   pad->map.queued = NULL;
 
+  if (pad->map.index_keypoints_array) {
+    g_tree_destroy (pad->map.index_keypoints_tree);
+    g_array_free (pad->map.index_keypoints_array, TRUE);
+  }
+
   /* clear continued pages */
   g_list_foreach (pad->continued, (GFunc) gst_ogg_page_free, NULL);
   g_list_free (pad->continued);
@@ -217,22 +301,14 @@ gst_ogg_pad_src_query (GstPad * pad, GstQuery * query)
     case GST_QUERY_DURATION:
     {
       GstFormat format;
-      gint64 total_time;
 
       gst_query_parse_duration (query, &format, NULL);
       /* can only get position in time */
       if (format != GST_FORMAT_TIME)
         goto wrong_format;
 
-      if (ogg->seekable) {
-        /* we must return the total seekable length */
-        total_time = ogg->total_time;
-      } else {
-        /* in non-seek mode we can answer the query and we must return -1 */
-        total_time = -1;
-      }
-
-      gst_query_set_duration (query, GST_FORMAT_TIME, total_time);
+      /* total_time may be valid (pull mode or push mode with index) or -1 */
+      gst_query_set_duration (query, GST_FORMAT_TIME, ogg->total_time);
       break;
     }
     case GST_QUERY_SEEKING:
@@ -365,7 +441,7 @@ gst_ogg_pad_reset (GstOggPad * pad)
 }
 
 /* called when the skeleton fishead is found. Caller ensures the packet is
- * precisely the correct size; we don't re-check this here. */
+ * at least Ogg Skeleton 3.0-sized; we don't re-check this here. */
 static void
 gst_ogg_pad_parse_skeleton_fishead (GstOggPad * pad, ogg_packet * packet)
 {
@@ -388,6 +464,32 @@ gst_ogg_pad_parse_skeleton_fishead (GstOggPad * pad, ogg_packet * packet)
   ogg->have_fishead = TRUE;
   pad->map.is_skeleton = TRUE;
   pad->start_time = GST_CLOCK_TIME_NONE;
+
+  /* Ogg Skeleton 3.2 streams provide additional information in the header */
+  if (packet->bytes >= SKELETON_FISHEAD_3_2_MIN_SIZE) {
+    gint64 firstsampletime_n, firstsampletime_d;
+    gint64 lastsampletime_n, lastsampletime_d;
+    gint64 firstsampletime, lastsampletime;
+
+    firstsampletime_n = GST_READ_UINT64_LE (data + 64);
+    firstsampletime_d = GST_READ_UINT64_LE (data + 72);
+    lastsampletime_n = GST_READ_UINT64_LE (data + 80);
+    lastsampletime_d = GST_READ_UINT64_LE (data + 88);
+
+    if (firstsampletime_d > 0)
+      firstsampletime = gst_util_uint64_scale (GST_SECOND,
+          firstsampletime_n, firstsampletime_d);
+
+    if (lastsampletime_d > 0)
+      lastsampletime = gst_util_uint64_scale (GST_SECOND,
+          lastsampletime_n, lastsampletime_d);
+
+    /* derive a duration if we don't already have one */
+    if (ogg->total_time == GST_CLOCK_TIME_NONE) {
+      ogg->total_time = lastsampletime - firstsampletime;
+    }
+  }
+
   GST_INFO_OBJECT (ogg, "skeleton fishead parsed (basetime: %"
       GST_TIME_FORMAT ", prestime: %" GST_TIME_FORMAT ")",
       GST_TIME_ARGS (ogg->basetime), GST_TIME_ARGS (ogg->prestime));
@@ -433,6 +535,118 @@ gst_ogg_pad_parse_skeleton_fisbone (GstOggPad * pad, ogg_packet * packet)
   }
 }
 
+/* function called when an Ogg Skeleton 3.2 index is found. Caller ensures
+ * that the packet length is sufficient */
+static void
+gst_ogg_pad_parse_skeleton_index (GstOggPad * pad, ogg_packet * packet)
+{
+  GstOggPad *index_pad;
+  guint32 serialno;
+  guint64 num_keypoints, num_malformed_keypoints, i;
+  guint64 req_bytes;
+  gint64 timestamp_d;
+  guint8 *data = packet->packet;
+
+  serialno = GST_READ_UINT32_LE (data + 6);
+
+  index_pad = gst_ogg_chain_get_stream (pad->chain, serialno);
+  if (index_pad) {
+    if (index_pad->map.have_index)
+      /* already parsed */
+      return;
+
+    index_pad->map.have_index = TRUE;
+
+    num_keypoints = GST_READ_UINT64_LE (data + 10);
+    timestamp_d = (gint64)GST_READ_UINT64_LE (data + 18);
+
+    /* guard division-by-zero */
+    if (timestamp_d == 0) {
+      GST_WARNING_OBJECT (pad->ogg,
+          "skeleton index for stream %" G_GUINT32_FORMAT
+          " has zero timestamp denominator", serialno);
+      timestamp_d = 1;
+    }
+
+    /* sanitise keypoint region to guard buffer overflow */
+    req_bytes = SKELETON_INDEX_HEADER_SIZE +
+        (num_keypoints * SKELETON_INDEX_KEYPOINT_SIZE);
+
+    if (num_keypoints > SKELETON_INDEX_MAX_KEYPOINTS ||
+        req_bytes > packet->bytes)
+    {
+      GST_WARNING_OBJECT (pad->ogg,
+          "skeleton index for stream %" G_GUINT32_FORMAT
+          " has malformed size (%" G_GUINT64_FORMAT
+          " keypoints in %ld bytes)", serialno, num_keypoints, packet->bytes);
+      return;
+    }
+
+    /* build an array and balanced binary tree of
+     * (time,offset,checksum) keypoint records */
+    if (num_keypoints == 0)
+      return;
+
+    index_pad->map.index_keypoints_array = g_array_sized_new (FALSE,
+        FALSE, sizeof(GstOggIndexKeypoint), num_keypoints);
+    index_pad->map.index_keypoints_tree = g_tree_new (gst_ogg_index_keypoint_compare);
+
+    for (i = 0; i < num_keypoints; ++ i) {
+      guint8 *keypoint_data;
+      guint64 prestime_n;
+      GstOggIndexKeypoint *keypoint;
+
+      keypoint_data = data + SKELETON_INDEX_HEADER_SIZE
+          + (i * SKELETON_INDEX_KEYPOINT_SIZE);
+      keypoint = &g_array_index (index_pad->map.index_keypoints_array,
+          GstOggIndexKeypoint, i);
+
+      keypoint->page_offset = GST_READ_UINT64_LE (keypoint_data);
+      keypoint->page_checksum = GST_READ_UINT32_LE (keypoint_data + 8);
+      prestime_n = GST_READ_UINT64_LE (keypoint_data + 12);
+      keypoint->prestime = gst_util_uint64_scale (GST_SECOND,
+          prestime_n, timestamp_d);
+
+      /* sanitise keypoint if the stream length is known;
+       * for information, not security */
+      if (pad->ogg->length > 0 && keypoint->page_offset >= pad->ogg->length) {
+        keypoint->page_offset = 0;
+        keypoint->page_checksum = 0;
+        keypoint->prestime = 0;
+
+        ++ num_malformed_keypoints;
+        continue;
+      }
+
+      g_tree_insert (index_pad->map.index_keypoints_tree, keypoint, keypoint);
+    }
+
+    if (num_malformed_keypoints > 0) {
+      GST_WARNING_OBJECT (pad->ogg,
+          "skeleton index for stream %" G_GUINT32_FORMAT
+          " has %" G_GUINT64_FORMAT
+          " malformed keypoints which exceed stream length",
+          serialno, num_malformed_keypoints);
+
+      if (num_malformed_keypoints == num_keypoints) {
+        /* if all keypoints are malformed, discard the index */
+        g_tree_destroy (pad->map.index_keypoints_tree);
+        g_array_free (pad->map.index_keypoints_array, TRUE);
+        pad->map.index_keypoints_tree = NULL;
+        pad->map.index_keypoints_array = NULL;
+      }
+    }
+
+    if (num_malformed_keypoints < num_keypoints)
+      /* stream is now seekable if it wasn't already */
+      pad->ogg->seekable = TRUE;
+  } else {
+    GST_WARNING_OBJECT (pad->ogg,
+        "found skeleton index for an unknown stream %" G_GUINT32_FORMAT,
+        serialno);
+  }
+}
+
 static GstBuffer *
 gst_ogg_demux_buffer_from_packet (ogg_packet * packet)
 {
@@ -595,6 +809,28 @@ gst_ogg_demux_chain_peer (GstOggPad * pad, ogg_packet * packet)
   if (pad->discont) {
     GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DISCONT);
     pad->discont = FALSE;
+
+    /* in push mode, a NEWSEGMENT event is required here -
+     * not sent earlier to support non-flushing seek */
+    if (!ogg->pull_mode) {
+      GstEvent *newsegment;
+
+      ogg->segment.start = out_timestamp;
+      ogg->segment.time = out_timestamp;
+
+      GST_INFO_OBJECT (ogg, "sending new segment: rate %g applied_rate %g "
+          "start: %" GST_TIME_FORMAT ", stop: %" GST_TIME_FORMAT
+          ", time: %" GST_TIME_FORMAT " to pad %" GST_PTR_FORMAT,
+          ogg->segment.rate, ogg->segment.applied_rate,
+          GST_TIME_ARGS (ogg->segment.start), GST_TIME_ARGS (ogg->segment.stop),
+          GST_TIME_ARGS (ogg->segment.time), pad);
+
+      newsegment = gst_event_new_new_segment_full (FALSE,
+          ogg->segment.rate, ogg->segment.applied_rate,
+          ogg->segment.format, ogg->segment.start, ogg->segment.stop, ogg->segment.time);
+
+      gst_pad_push_event (GST_PAD_CAST (pad), newsegment);
+    }
   }
 
   pad->last_stop = ogg->segment.last_stop;
@@ -678,7 +914,7 @@ gst_ogg_pad_submit_packet (GstOggPad * pad, ogg_packet * packet)
       pad->map.serialno);
 
   if (!pad->have_type) {
-    if (!ogg->have_fishead && packet->bytes == SKELETON_FISHEAD_SIZE &&
+    if (!ogg->have_fishead && packet->bytes >= SKELETON_FISHEAD_MIN_SIZE &&
         !memcmp (packet->packet, "fishead\0", 8)) {
       gst_ogg_pad_parse_skeleton_fishead (pad, packet);
     }
@@ -698,6 +934,11 @@ gst_ogg_pad_submit_packet (GstOggPad * pad, ogg_packet * packet)
     gst_ogg_pad_parse_skeleton_fisbone (pad, packet);
   }
 
+  if (ogg->have_fishead && packet->bytes >= SKELETON_INDEX_HEADER_SIZE &&
+      !memcmp (packet->packet, "index\0", 6)) {
+    gst_ogg_pad_parse_skeleton_index (pad, packet);
+  }
+
   granule = gst_ogg_stream_granulepos_to_granule (&pad->map,
       packet->granulepos);
   if (granule != -1) {
@@ -1192,6 +1433,7 @@ gst_ogg_demux_init (GstOggDemux * ogg, GstOggDemuxClass * g_class)
   ogg->chains = g_array_new (FALSE, TRUE, sizeof (GstOggChain *));
 
   ogg->newsegment = NULL;
+  ogg->total_time = GST_CLOCK_TIME_NONE;
 }
 
 static void
@@ -1225,6 +1467,18 @@ gst_ogg_demux_sink_event (GstPad * pad, GstEvent * event)
       GST_DEBUG_OBJECT (ogg, "got a new segment event");
       ogg_sync_reset (&ogg->sync);
       gst_event_unref (event);
+
+      /* in push mode, ignore packets until new timestamp available */
+      if (!ogg->pull_mode && ogg->current_chain) {
+        gint i;
+        GstOggChain *chain = ogg->current_chain;
+
+        for (i = 0; i < chain->streams->len; i++) {
+          GstOggPad *stream_pad = g_array_index (chain->streams, GstOggPad *, i);
+          stream_pad->current_granule = -1;
+        }
+      }
+
       res = TRUE;
       break;
     case GST_EVENT_EOS:
@@ -1955,6 +2209,15 @@ seek_error:
 static gboolean
 gst_ogg_demux_perform_seek (GstOggDemux * ogg, GstEvent * event)
 {
+  if (ogg->pull_mode)
+    return gst_ogg_demux_perform_seek_pull (ogg, event);
+  else
+    return gst_ogg_demux_perform_seek_push (ogg, event);
+}
+
+static gboolean
+gst_ogg_demux_perform_seek_pull (GstOggDemux * ogg, GstEvent * event)
+{
   GstOggChain *chain = NULL;
   gboolean res;
   gboolean flush, accurate, keyframe;
@@ -2198,6 +2461,92 @@ no_chain:
   }
 }
 
+static gboolean
+gst_ogg_demux_perform_seek_push (GstOggDemux * ogg, GstEvent * event)
+{
+  gdouble rate;
+  GstFormat format;
+  GstSeekFlags flags;
+  GstSeekType seek_types[2];
+  gint64 times[2];
+  gint time_idx, stream_idx;
+  GstEvent *bevent;
+  gint64 byte_offsets[2] = { -1, -1 };
+
+  gst_event_parse_seek (event, &rate, &format, &flags, &seek_types[0],
+      &times[0], &seek_types[1], &times[1]);
+
+  GST_DEBUG_OBJECT (ogg, "seek event, rate: %f start: %" GST_TIME_FORMAT
+      " stop: %" GST_TIME_FORMAT, rate, GST_TIME_ARGS (times[0]),
+      GST_TIME_ARGS (times[1]));
+
+  if (format == GST_FORMAT_BYTES) {
+    GST_DEBUG_OBJECT (ogg, "seek not supported on format %d", format);
+    return FALSE;
+  }
+
+  GST_DEBUG_OBJECT (ogg, "seek - trying directly upstream first");
+
+  /* first try original format seek */
+  gst_event_ref (event);
+  if (gst_pad_push_event (ogg->sinkpad, event))
+    return TRUE;
+
+  if (format != GST_FORMAT_TIME) {
+    /* from here down, we only support time based seeks */
+    GST_DEBUG_OBJECT (ogg, "seek not supported on format %d", format);
+    return FALSE;
+  }
+
+  GST_DEBUG_OBJECT (ogg, "seek - trying with index metadata");
+
+  for (time_idx = 0; time_idx < 2; time_idx++) {
+    gint64 nearest_time;
+
+    if (seek_types[time_idx] == GST_SEEK_TYPE_NONE)
+      continue;
+
+    if (seek_types[time_idx] != GST_SEEK_TYPE_SET) {
+      /* relative seeks are unsupported */
+      GST_DEBUG_OBJECT (ogg, "seek not supported on type %d",
+          seek_types[time_idx]);
+      return FALSE;
+    }
+
+    /* search the keypoint index of each stream for the
+     * record nearest to the requested time */
+    nearest_time = -1;
+
+    for (stream_idx = 0; stream_idx < ogg->current_chain->streams->len; stream_idx++) {
+      GstOggPad *pad;
+      GstOggIndexKeypoint *nearest_keypoint;
+
+      pad = g_array_index (ogg->current_chain->streams,
+          GstOggPad *, stream_idx);
+      nearest_keypoint = gst_ogg_index_keypoint_find_nearest (pad, times[time_idx]);
+
+      if (nearest_keypoint) {
+        /* if this stream has a nearer keypoint than an earlier one,
+           go with it - but is this best? or prioritise video? */
+        if (nearest_time == -1 ||
+            ABS(nearest_keypoint->prestime - times[time_idx]) <
+            ABS(nearest_time - times[time_idx]))
+        {
+          byte_offsets[time_idx] = nearest_keypoint->page_offset;
+          nearest_time = nearest_keypoint->prestime;
+        }
+      }
+    }
+  }
+
+  GST_DEBUG_OBJECT (ogg, "in bytes bstart %" G_GINT64_FORMAT " bstop %"
+      G_GINT64_FORMAT, byte_offsets[0], byte_offsets[1]);
+  bevent = gst_event_new_seek (rate, GST_FORMAT_BYTES, flags, seek_types[0],
+      byte_offsets[0], seek_types[1], byte_offsets[1]);
+
+  return gst_pad_push_event (ogg->sinkpad, bevent);
+}
+
 /* finds each bitstream link one at a time using a bisection search
  * (has to begin by knowing the offset of the lb's initial page).
  * Recurses for each link so it can alloc the link storage after
@@ -3173,6 +3522,7 @@ gst_ogg_demux_sink_activate_push (GstPad * sinkpad, gboolean active)
 
   ogg = GST_OGG_DEMUX (GST_OBJECT_PARENT (sinkpad));
 
+  ogg->pull_mode = FALSE;
   ogg->seekable = FALSE;
 
   return TRUE;
@@ -3189,6 +3539,7 @@ gst_ogg_demux_sink_activate_pull (GstPad * sinkpad, gboolean active)
   ogg = GST_OGG_DEMUX (GST_OBJECT_PARENT (sinkpad));
 
   if (active) {
+    ogg->pull_mode = TRUE;
     ogg->need_chains = TRUE;
     ogg->seekable = TRUE;
 
diff --git a/ext/ogg/gstoggdemux.h b/ext/ogg/gstoggdemux.h
index f9a8a96..390abb1 100644
--- a/ext/ogg/gstoggdemux.h
+++ b/ext/ogg/gstoggdemux.h
@@ -136,6 +136,7 @@ struct _GstOggDemux
   gint64 read_offset;
   gint64 offset;
 
+  gboolean pull_mode;
   gboolean seekable;
   gboolean running;
 
diff --git a/ext/ogg/gstoggstream.c b/ext/ogg/gstoggstream.c
index 0f8e39b..d077a9a 100644
--- a/ext/ogg/gstoggstream.c
+++ b/ext/ogg/gstoggstream.c
@@ -1282,6 +1282,11 @@ gst_ogg_stream_setup_map (GstOggStream * pad, ogg_packet * packet)
       if (ret) {
         GST_DEBUG ("got stream type %" GST_PTR_FORMAT, pad->caps);
         pad->map = i;
+
+        /* N:1 mapping of mappers to setup_funcs may need late caps setup */
+        if(!pad->caps) {
+          pad->caps = gst_caps_new_simple (mappers[i].media_type, NULL);
+        }
         return TRUE;
       } else {
         GST_WARNING ("mapper '%s' did not accept setup header",
diff --git a/ext/ogg/gstoggstream.h b/ext/ogg/gstoggstream.h
index 165c2a0..6ea68a0 100644
--- a/ext/ogg/gstoggstream.h
+++ b/ext/ogg/gstoggstream.h
@@ -46,6 +46,7 @@ struct _GstOggStream
   gint map;
   gboolean is_skeleton;
   gboolean have_fisbone;
+  gboolean have_index;
   gint granulerate_n;
   gint granulerate_d;
   guint32 preroll;
@@ -55,6 +56,10 @@ struct _GstOggStream
   gint64 accumulated_granule;
   gint frame_size;
 
+  /* Ogg Skeleton 3.2 index metadata */
+  GArray *index_keypoints_array;
+  GTree *index_keypoints_tree;
+
   GstCaps *caps;
   
   /* vorbis stuff */
