nleobject: stop using media-duration-factor
authorHenry Wilkes <hwilkes@igalia.com>
Wed, 8 Apr 2020 16:11:14 +0000 (17:11 +0100)
committerGStreamer Merge Bot <gitlab-merge-bot@gstreamer-foundation.org>
Wed, 29 Apr 2020 12:32:52 +0000 (12:32 +0000)
The property had been deprecated and is unused.

This property is not needed. Any internal time effect that an nleoperation
wraps is itself responsible for converting seek/segment timestamps.
Previously, the ghostpads were performing a rate conversion after the
rate element had already done so, essentially doubling their effect on
seeks and segment times. This was always unnecessary, but went unnoticed
by the tempochange test because it was using an identity element rather
than an actual rate-changing element.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-editing-services/-/merge_requests/160>

ges/ges-effect.c
ges/ges-track-element.c
plugins/nle/nleghostpad.c
plugins/nle/nleobject.c
plugins/nle/nleobject.h
plugins/nle/nleoperation.c
tests/check/ges/tempochange.c
tests/check/nle/tempochange.c

index 9d69ee5..4de3aaf 100644 (file)
@@ -132,33 +132,6 @@ property_name_compare (gconstpointer s1, gconstpointer s2)
 }
 
 static void
-_set_child_property (GESTimelineElement * self G_GNUC_UNUSED, GObject * child,
-    GParamSpec * pspec, GValue * value)
-{
-  GESEffectClass *klass = GES_EFFECT_GET_CLASS (self);
-  gchar *full_property_name;
-
-  GES_TIMELINE_ELEMENT_CLASS
-      (ges_effect_parent_class)->set_child_property (self, child, pspec, value);
-
-  full_property_name = g_strdup_printf ("%s::%s", G_OBJECT_TYPE_NAME (child),
-      pspec->name);
-
-  if (g_list_find_custom (klass->rate_properties, full_property_name,
-          property_name_compare)) {
-    GstElement *nleobject =
-        ges_track_element_get_nleobject (GES_TRACK_ELEMENT (self));
-    gdouble media_duration_factor =
-        ges_timeline_element_get_media_duration_factor (self);
-
-    g_object_set (nleobject, "media-duration-factor", media_duration_factor,
-        NULL);
-  }
-
-  g_free (full_property_name);
-}
-
-static void
 ges_effect_get_property (GObject * object,
     guint property_id, GValue * value, GParamSpec * pspec)
 {
@@ -193,11 +166,9 @@ ges_effect_class_init (GESEffectClass * klass)
 {
   GObjectClass *object_class;
   GESTrackElementClass *obj_bg_class;
-  GESTimelineElementClass *element_class;
 
   object_class = G_OBJECT_CLASS (klass);
   obj_bg_class = GES_TRACK_ELEMENT_CLASS (klass);
-  element_class = GES_TIMELINE_ELEMENT_CLASS (klass);
 
   object_class->get_property = ges_effect_get_property;
   object_class->set_property = ges_effect_set_property;
@@ -205,7 +176,6 @@ ges_effect_class_init (GESEffectClass * klass)
   object_class->finalize = ges_effect_finalize;
 
   obj_bg_class->create_element = ges_effect_create_element;
-  element_class->set_child_property = _set_child_property;
 
   klass->rate_properties = NULL;
   ges_effect_class_register_rate_property (klass, "scaletempo", "rate");
index 2f9eff3..a8adc01 100644 (file)
@@ -262,7 +262,6 @@ ges_track_element_set_asset (GESExtractable * extractable, GESAsset * asset)
 {
   GESTrackElementClass *class;
   GstElement *nleobject;
-  gdouble media_duration_factor;
   gchar *tmp;
   GESTrackElement *object = GES_TRACK_ELEMENT (extractable);
 
@@ -298,12 +297,6 @@ ges_track_element_set_asset (GESExtractable * extractable, GESAsset * asset)
       "duration", GES_TIMELINE_ELEMENT_DURATION (object),
       "priority", GES_TIMELINE_ELEMENT_PRIORITY (object),
       "active", object->active & object->priv->layer_active, NULL);
-
-  media_duration_factor =
-      ges_timeline_element_get_media_duration_factor (GES_TIMELINE_ELEMENT
-      (object));
-  g_object_set (object->priv->nleobject,
-      "media-duration-factor", media_duration_factor, NULL);
 }
 
 static void
index 75778c9..cb1e5ec 100644 (file)
@@ -98,6 +98,12 @@ nle_object_translate_incoming_seek (NleObject * object, GstEvent * event)
     GST_LOG_OBJECT (object, "Setting stop to %" GST_TIME_FORMAT,
         GST_TIME_ARGS (nstop));
   } else {
+    /* NOTE: for an element that is upstream from a time effect we do not
+     * want to limit the seek to the object->stop because a time effect
+     * can reasonably increase the final time.
+     * In such situations, the seek stop should be set once by the
+     * source pad of the most downstream object (highest priority in the
+     * nlecomposition). */
     GST_DEBUG_OBJECT (object, "Limiting end of seek to media_stop");
     nle_object_to_media_time (object, object->stop, &nstop);
     if (nstop > G_MAXINT64)
@@ -190,6 +196,9 @@ translate_outgoing_seek (NleObject * object, GstEvent * event)
     GST_LOG_OBJECT (object, "Setting stop to %" GST_TIME_FORMAT,
         GST_TIME_ARGS (nstop));
   } else {
+    /* NOTE: when we have time effects, the object stop is not the
+     * correct stop limit. Therefore, the seek stop time should already
+     * be set at this point */
     GST_DEBUG_OBJECT (object, "Limiting end of seek to stop");
     nstop = object->stop;
     if (nstop > G_MAXINT64)
@@ -423,6 +432,9 @@ translate_incoming_duration_query (NleObject * object, GstQuery * query)
     return FALSE;
   }
 
+  /* NOTE: returns the duration of the object, but this is not the same
+   * as the source duration when time effects are used. Nor is it the
+   * duration of the current nlecomposition stack */
   gst_query_set_duration (query, GST_FORMAT_TIME, object->duration);
 
   return TRUE;
index 6e16f8e..1c6266b 100644 (file)
@@ -248,6 +248,10 @@ nle_object_class_init (NleObjectClass * klass)
    * relation between the rate of media entering and leaving this object. I.e.
    * if object pulls data at twice the speed it sends it (e.g. `pitch
    * tempo=2.0`), this value is set to 2.0.
+   *
+   * Deprecated: 1.18: This property is ignored since the wrapped
+   * #GstElement-s themselves should internally perform any additional time
+   * translations.
    */
   properties[PROP_MEDIA_DURATION_FACTOR] =
       g_param_spec_double ("media-duration-factor", "Media duration factor",
@@ -292,8 +296,6 @@ nle_object_init (NleObject * object, NleObjectClass * klass)
   object->segment_rate = 1.0;
   object->segment_start = -1;
   object->segment_stop = -1;
-  object->media_duration_factor = 1.0;
-  object->recursive_media_duration_factor = 1.0;
 
   object->srcpad = nle_object_ghost_pad_no_target (object,
       "src", GST_PAD_SRC,
@@ -326,15 +328,41 @@ nle_object_dispose (GObject * object)
  * @objecttime: The #GstClockTime we want to convert
  * @mediatime: A pointer on a #GstClockTime to fill
  *
- * Converts a #GstClockTime from the object (container) context to the media context
+ * Converts a #GstClockTime timestamp received from another nleobject pad
+ * in the same nlecomposition, or from the nlecomposition itself, to an
+ * internal source time.
+ *
+ * If the object is furthest downstream in the nlecomposition (highest
+ * priority in the current stack), this will convert the timestamp from
+ * the composition coordinate time to the internal source coordinate time
+ * of the object.
+ *
+ * If the object is upstream from another nleobject, then this can convert
+ * the timestamp received from the downstream sink pad to the internal
+ * source coordinates of the object, to be passed to its internal
+ * elements.
+ *
+ * If the object is downstream from another nleobject, then this can
+ * convert the timestamp received from the upstream source pad to the
+ * internal sink coordinates of the object, to be passed to its internal
+ * elements.
  *
- * Returns: TRUE if @objecttime was within the limits of the @object start/stop time,
- * FALSE otherwise
+ * In these latter two cases, the timestamp should have been converted
+ * by the peer pad using nle_media_to_object_time().
+ *
+ * Note, if an object introduces a time effect, it must have a 0 in-point
+ * and the same #nleobject:start and #nleobject:duration as all the other
+ * objects that are further upstream.
+ *
+ * Returns: TRUE if @objecttime was below the @object start time,
+ * FALSE otherwise.
  */
 gboolean
 nle_object_to_media_time (NleObject * object, GstClockTime otime,
     GstClockTime * mtime)
 {
+  gboolean ret = TRUE;
+
   g_return_val_if_fail (mtime, FALSE);
 
   GST_DEBUG_OBJECT (object, "ObjectTime : %" GST_TIME_FORMAT,
@@ -345,39 +373,61 @@ nle_object_to_media_time (NleObject * object, GstClockTime otime,
       "Media start: %" GST_TIME_FORMAT, GST_TIME_ARGS (object->start),
       GST_TIME_ARGS (object->stop), GST_TIME_ARGS (object->inpoint));
 
-  /* limit check */
-  if (G_UNLIKELY ((otime < object->start))) {
-    GST_DEBUG_OBJECT (object, "ObjectTime is before start");
-    *mtime = (object->inpoint == GST_CLOCK_TIME_NONE) ? 0 : object->inpoint;
-    return FALSE;
+  if (G_UNLIKELY (!GST_CLOCK_TIME_IS_VALID (otime))) {
+    GST_DEBUG_OBJECT (object, "converting none object time to none");
+    *mtime = GST_CLOCK_TIME_NONE;
+    return TRUE;
   }
 
-  if (G_UNLIKELY ((otime >= object->stop))) {
-    GST_DEBUG_OBJECT (object, "ObjectTime is after stop");
-    if (G_LIKELY (GST_CLOCK_TIME_IS_VALID (object->inpoint)))
-      *mtime =
-          object->inpoint +
-          object->duration * object->recursive_media_duration_factor;
-    else
-      *mtime =
-          (object->stop -
-          object->start) * object->recursive_media_duration_factor;
-    return FALSE;
+  /* We do not allow @otime to be below the start of the object.
+   * If it was below, then the object would have a negative external
+   * source/sink time.
+   *
+   * Note that ges only supports time effects that map the time 0 to
+   * 0. Such time effects would also not produce an external timestamp
+   * below start, nor can they receive such a timestamp. */
+  if (G_UNLIKELY ((otime < object->start))) {
+    GST_DEBUG_OBJECT (object, "ObjectTime is before start");
+    otime = object->start;
+    ret = FALSE;
   }
+  /* NOTE: if an nlecomposition contains time effect operations, then
+   * @otime can reasonably exceed the stop time of the object. So we
+   * do not limit it here. */
+
+  /* first we convert the timestamp to the object's external source/sink
+   * coordinates:
+   * + For an object that is furthest downstream, we translate from the
+   *   composition coordinates to the external source coordinates by
+   *   subtracting the object start.
+   * + For an object that is upstream from d_object, we need to
+   *   translate from its external sink coordinates to our external
+   *   source coordinates. This is done by adding
+   *     (d_object->start - object->start)
+   *   However, the sink pad of d_object should have already added the
+   *   d_object->start to the timestamp (see nle_media_to_object_time)
+   *   so we also only need to subtract the object start.
+   * + For an object that is downstream from u_object, we need to
+   *   translate from its external source coordinates to our external
+   *   sink coordinates. This is similarly done by adding
+   *     (u_object->start - object->start)
+   *   However, the source pad of u_object should have already added the
+   *   u_object->start to the timestamp (see nle_media_to_object_time)
+   *   so we also only need to subtract the object start.
+   */
+  *mtime = otime - object->start;
 
-  if (G_UNLIKELY (object->inpoint == GST_CLOCK_TIME_NONE)) {
-    /* no time shifting, for live sources ? */
-    *mtime = (otime - object->start) * object->recursive_media_duration_factor;
-  } else {
-    *mtime =
-        (otime - object->start) * object->recursive_media_duration_factor +
-        object->inpoint;
-  }
+  /* we then convert the timestamp from the object's external source/sink
+   * coordinates to its internal source/sink coordinates, to be used by
+   * internal elements that the object wraps. This is done by adding
+   * the object in-point. */
+  if (G_LIKELY (GST_CLOCK_TIME_IS_VALID (object->inpoint)))
+    *mtime += object->inpoint;
 
   GST_DEBUG_OBJECT (object, "Returning MediaTime : %" GST_TIME_FORMAT,
       GST_TIME_ARGS (*mtime));
 
-  return TRUE;
+  return ret;
 }
 
 /**
@@ -386,10 +436,28 @@ nle_object_to_media_time (NleObject * object, GstClockTime otime,
  * @mediatime: The #GstClockTime we want to convert
  * @objecttime: A pointer on a #GstClockTime to fill
  *
- * Converts a #GstClockTime from the media context to the object (container) context
+ * Converts a #GstClockTime timestamp from an internal time to an
+ * nleobject pad time.
+ *
+ * If the object is furthest downstream in an nlecomposition (highest
+ * priority in the current stack), this will convert the timestamp from
+ * the internal source coordinate time of the object to the composition
+ * coordinate time.
  *
- * Returns: TRUE if @objecttime was within the limits of the @object media start/stop time,
- * FALSE otherwise
+ * If the object is upstream from another nleobject, then this can convert
+ * the timestamp from the internal source coordinates of the object to be
+ * sent to the downstream sink pad.
+ *
+ * If the object is downstream from another nleobject, then this can
+ * convert the timestamp from the internal sink coordinates of the object
+ * to be sent to the upstream source pad.
+ *
+ * Note, if an object introduces a time effect, it must have a 0 in-point
+ * and the same #nleobject:start and #nleobject:duration as all the other
+ * objects that are further upstream.
+ *
+ * Returns: TRUE if @objecttime was below the @object in-point time,
+ * FALSE otherwise.
  */
 
 gboolean
@@ -406,19 +474,35 @@ nle_media_to_object_time (NleObject * object, GstClockTime mtime,
       "inpoint  %" GST_TIME_FORMAT, GST_TIME_ARGS (object->start),
       GST_TIME_ARGS (object->stop), GST_TIME_ARGS (object->inpoint));
 
+  if (G_UNLIKELY (!GST_CLOCK_TIME_IS_VALID (mtime))) {
+    GST_DEBUG_OBJECT (object, "converting none media time to none");
+    *otime = GST_CLOCK_TIME_NONE;
+    return TRUE;
+  }
 
-  /* limit check */
-  if (G_UNLIKELY ((object->inpoint != GST_CLOCK_TIME_NONE)
+  /* the internal time should never go below the in-point! */
+  if (G_UNLIKELY (GST_CLOCK_TIME_IS_VALID (object->inpoint)
           && (mtime < object->inpoint))) {
     GST_DEBUG_OBJECT (object, "media time is before inpoint, forcing to start");
     *otime = object->start;
     return FALSE;
   }
 
-  if (G_LIKELY (object->inpoint != GST_CLOCK_TIME_NONE)) {
-    *otime = mtime - object->inpoint + object->start;
-  } else
-    *otime = mtime + object->start;
+  /* first we convert the timestamp to the object's external source/sink
+   * coordinates by removing the in-point */
+  if (G_LIKELY (GST_CLOCK_TIME_IS_VALID (object->inpoint)))
+    *otime = mtime - object->inpoint;
+  else
+    *otime = mtime;
+
+  /* then we convert the timestamp by adding start.
+   * If the object is furthest downstream, this will translate it from
+   * the external source coordinates to the composition coordinates.
+   * Otherwise, this will perform part of the conversion from the object's
+   * source/sink coordinates to the downstream/upstream sink/source
+   * coordinates (the conversion is completed in
+   * nle_object_to_media_time). */
+  *otime += object->start;
 
   GST_DEBUG_OBJECT (object, "Returning ObjectTime : %" GST_TIME_FORMAT,
       GST_TIME_ARGS (*otime));
@@ -563,7 +647,12 @@ nle_object_set_property (GObject * object, guint prop_id,
         GST_OBJECT_FLAG_UNSET (nleobject, NLE_OBJECT_EXPANDABLE);
       break;
     case PROP_MEDIA_DURATION_FACTOR:
-      nleobject->media_duration_factor = g_value_get_double (value);
+    {
+      gdouble val = g_value_get_double (value);
+      if (val != 1.0)
+        g_warning ("Ignoring media-duration-factor value of %g since the "
+            "property is deprecated", val);
+    }
       break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -604,7 +693,8 @@ nle_object_get_property (GObject * object, guint prop_id,
       g_value_set_boolean (value, NLE_OBJECT_IS_EXPANDABLE (object));
       break;
     case PROP_MEDIA_DURATION_FACTOR:
-      g_value_set_double (value, nleobject->media_duration_factor);
+      g_warning ("The media-duration-factor property is deprecated");
+      g_value_set_double (value, 1.0);
       break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
index 5af9717..1e0272e 100644 (file)
@@ -126,12 +126,6 @@ struct _NleObject
   gint64 segment_start;
   gint64 segment_stop;
 
-  /* the rate at which this object speeds up or slows down media */
-  gdouble media_duration_factor;
-  /* the rate at which this object and all of its children speed up or slow
-   * down media */
-  gdouble recursive_media_duration_factor;
-
   gboolean in_composition;
 };
 
index 6f37eaa..62e27a7 100644 (file)
  * A NleOperation performs a transformation or mixing operation on the
  * data from one or more #NleSources, which is used to implement filters or
  * effects.
+ *
+ * ## Time Effects
+ *
+ * An #nleoperation that wraps a #GstElement that transforms seek and
+ * segment times is considered a time effect. Nle only tries to support
+ * time effect's whose overall seek transformation:
+ *
+ * + Maps the time `0` to `0`. So initial time-shifting effects are
+ *   excluded (the #nlesource:inpoint can sometimes be used instead).
+ * + Is monotonically increasing. So reversing effects, and effects that
+ *   jump backwards in the stream are excluded.
+ * + Can handle a reasonable #GstClockTime, relative to the project. So
+ *   this would exclude a time effect with an extremely large speed-up
+ *   that would cause the converted #GstClockTime seeks to overflow.
+ * + Is 'continuously reversible'. This essentially means that for every
+ *   seek position found on the sink pad of the element, we can, to 'good
+ *   enough' accuracy, calculate the corresponding seek position that was
+ *   received on the source pad. Moreover, this calculation should
+ *   correspond to how the element transforms its #GstSegment
+ *   @time field. This is needed so that a seek can result in an accurate
+ *   segment.
+ *
+ * Note that a constant-rate-change effect that is not extremely fast or
+ * slow would satisfy these conditions.
+ *
+ * For such a time effect, they should be configured in nle such that:
+ *
+ * + Their #nleoperation:inpoint is `0`. Otherwise this will introduce
+ *   seeking problems in its #nlecomposition.
+ * + They must share the same #nleoperation:start and
+ *   #nleoperation:duration as all nleobjects of lower priority in its
+ *   #nlecomposition. Otherwise this will introduce jumps in playback.
+ *
+ * Note that, at the moment, nle only converts the #GstSegment
+ * @time field which means the other fields, such as @start and @stop, can
+ * end up with non-meaningful values when time effects are involved.
+ * Moreover, it does not convert #GstBuffer times either, which can result
+ * in them having non-meaningful values. In particular, this means that
+ * #GstControlBinding-s will not work well with #nlecomposition-s when
+ * they include time effects.
  */
 
 static GstStaticPadTemplate nle_operation_src_template =
index b537d91..ba38e12 100644 (file)
@@ -30,8 +30,6 @@ GST_START_TEST (test_tempochange)
   GESEffect *effect;
   GESTestClip *clip;
   GESClip *clip2, *clip3;
-  GList *tmp;
-  int found;
 
   ges_init ();
 
@@ -78,45 +76,6 @@ GST_START_TEST (test_tempochange)
   fail_unless_equals_int64 (_INPOINT (clip3), 7.5 * GST_SECOND);
   fail_unless_equals_int64 (_DURATION (clip3), 3 * GST_SECOND);
 
-#define MDF_OF_CLIP(x) \
-  ((NleObject *) ges_track_element_get_nleobject \
-    (ges_clip_find_track_element (GES_CLIP (x), track_audio, \
-    G_TYPE_NONE)))->media_duration_factor
-
-  fail_unless_equals_float (MDF_OF_CLIP (clip), 1.0);
-  fail_unless_equals_float (MDF_OF_CLIP (clip2), 1.5);
-  fail_unless_equals_float (MDF_OF_CLIP (clip3), 1.5);
-
-  found = 0;
-
-  for (tmp = GES_CONTAINER_CHILDREN (clip2); tmp; tmp = tmp->next) {
-    // A clip may have children other than the effect we added. As such, we
-    // need to find the child which is the pitch effect in order to check its
-    // value.
-    GstElement *nle = ges_track_element_get_nleobject (tmp->data);
-    if (strncmp (GST_OBJECT_NAME (nle), "GESEffect:", 10) == 0) {
-      fail_if (found);
-      found = 1;
-      fail_unless_equals_float (((NleObject *) nle)->media_duration_factor,
-          1.5);
-    }
-  }
-
-  fail_unless (found);
-
-  found = 0;
-  for (tmp = GES_CONTAINER_CHILDREN (clip3); tmp; tmp = tmp->next) {
-    GstElement *nle = ges_track_element_get_nleobject (tmp->data);
-    if (strncmp (GST_OBJECT_NAME (nle), "GESEffect:", 10) == 0) {
-      fail_if (found);
-      found = 1;
-      fail_unless_equals_float (((NleObject *) nle)->media_duration_factor,
-          1.5);
-    }
-  }
-
-  fail_unless (found);
-
   ges_layer_remove_clip (layer, (GESClip *) clip);
   ges_layer_remove_clip (layer, clip2);
   ges_layer_remove_clip (layer, clip3);
index f1ccf4e..2ac27d7 100644 (file)
 #include "common.h"
 #include "plugins/nle/nleobject.h"
 
-GST_START_TEST (test_tempochange)
+typedef struct _PadEventData
 {
-  GstElement *pipeline;
-  GstElement *comp, *source1, *def, *sink, *oper;
-  GList *segments = NULL;
-  GstBus *bus;
-  GstMessage *message;
-  gboolean carry_on, ret = FALSE;
-  CollectStructure *collect;
-  GstPad *sinkpad;
+  gchar *name;
+  guint expect_num_segments;
+  guint num_segments;
+  GArray *expect_segment_time;
+  GArray *expect_segment_num_seeks;
+  guint expect_num_seeks;
+  guint num_seeks;
+  GArray *expect_seek_start;
+  GArray *expect_seek_stop;
+  GArray *expect_seek_num_segments;
+  guint num_eos;
+  guint expect_num_eos;
+} PadEventData;
+
+#define _SEGMENT_FORMAT "flags: %i, rate: %g, applied_rate: %g, format: %i" \
+  ", base: %" G_GUINT64_FORMAT ", offset: %" G_GUINT64_FORMAT ", start: %" \
+  G_GUINT64_FORMAT ", stop: %" G_GUINT64_FORMAT ", time: %" G_GUINT64_FORMAT \
+  ", position: %" G_GUINT64_FORMAT ", duration: %" G_GUINT64_FORMAT
+
+#define _SEGMENT_ARGS(seg) (seg).flags, (seg).rate, (seg).applied_rate, \
+  (seg).format, (seg).base, (seg).offset, (seg).start, (seg).stop, \
+  (seg).time, (seg).position, (seg).duration
+
+static GstPadProbeReturn
+_test_pad_events (GstPad * pad, GstPadProbeInfo * info, PadEventData * data)
+{
+  guint num;
+  GstEvent *event = info->data;
+  if (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT) {
+    guint expect_num_seeks;
+    const GstSegment *segment;
+    /* copy the segment start, stop, position and duration since these are
+     * not yet translated by nleghostpad. Also, don't care about flags. */
+    GstSegment expect_segment;
+
+    gst_event_parse_segment (event, &segment);
+    GST_DEBUG ("%s segment: " _SEGMENT_FORMAT, data->name,
+        _SEGMENT_ARGS (*segment));
+
+    if (!GST_CLOCK_TIME_IS_VALID (segment->stop)) {
+      GST_DEBUG ("%s: ignoring pre-roll segment", data->name);
+      return GST_PAD_PROBE_OK;
+    }
+
+    data->num_segments++;
+    num = data->num_segments;
+
+    fail_unless (num <= data->expect_num_segments, "%s received %u "
+        "segments, more than the expected %u segments", data->name, num,
+        data->expect_num_segments);
+
+    expect_num_seeks =
+        g_array_index (data->expect_segment_num_seeks, gint, num - 1);
+
+    fail_unless (data->num_seeks == expect_num_seeks, "%s has received %u "
+        "segments, compared to %u seeks, but expected %u seeks",
+        data->name, num, data->num_seeks, expect_num_seeks);
+
+    /* copy the segment start, stop, position, duration, offset, base
+     * since these are not yet translated by nleghostpad. */
+    gst_segment_copy_into (segment, &expect_segment);
+    expect_segment.rate = 1.0;
+    expect_segment.applied_rate = 1.0;
+    expect_segment.format = GST_FORMAT_TIME;
+    expect_segment.time = g_array_index (data->expect_segment_time,
+        GstClockTime, num - 1);
+
+    fail_unless (gst_segment_is_equal (segment, &expect_segment),
+        "%s %uth segment is not equal to the expected. Received:\n"
+        _SEGMENT_FORMAT "\nExpected\n" _SEGMENT_FORMAT, data->name,
+        num - 1, _SEGMENT_ARGS (*segment), _SEGMENT_ARGS (expect_segment));
+
+  } else if (GST_EVENT_TYPE (event) == GST_EVENT_SEEK) {
+    gdouble rate;
+    GstFormat format;
+    GstClockTime expect;
+    gint64 start, stop;
+    GstSeekType start_type, stop_type;
+    guint expect_num_segments;
+
+    gst_event_parse_seek (event, &rate, &format, NULL, &start_type, &start,
+        &stop_type, &stop);
+
+    GST_DEBUG ("%s seek: rate: %g, start: %" G_GINT64_FORMAT ", stop: %"
+        G_GINT64_FORMAT, data->name, rate, start, stop);
+
+    data->num_seeks++;
+    num = data->num_seeks;
+
+    fail_unless (num <= data->expect_num_seeks, "%s received %u "
+        "seeks, more than the expected %u seeks", data->name, num,
+        data->expect_num_seeks);
+
+    expect_num_segments =
+        g_array_index (data->expect_seek_num_segments, gint, num - 1);
+
+    fail_unless (data->num_segments == expect_num_segments, "%s has "
+        "received %u seeks, compared to %u segments, but expected %u "
+        "segments", data->name, num, data->num_segments, expect_num_segments);
+
+    fail_unless (rate == 1.0, "%s %uth seek has a rate of %g rather than 1.0",
+        data->name, num - 1, rate);
+    fail_unless (format == GST_FORMAT_TIME, "%s %uth seek has a format of %i "
+        " than a time format", data->name, num - 1, format);
+
+    /* expect seek-set or seek-none */
+    fail_if (start_type == GST_SEEK_TYPE_END, "%s %uth seek-start is "
+        "seek-end", data->name, num - 1);
+    fail_if (stop_type == GST_SEEK_TYPE_END, "%s %uth seek-stop is "
+        "seek-end", data->name, num - 1);
+
+    expect = g_array_index (data->expect_seek_start, GstClockTime, num - 1);
+    fail_unless (start == expect, "%s %uth seek start is %" GST_TIME_FORMAT
+        ", rather than the expected %" GST_TIME_FORMAT, data->name, num - 1,
+        GST_TIME_ARGS (start), GST_TIME_ARGS (expect));
+
+    expect = g_array_index (data->expect_seek_stop, GstClockTime, num - 1);
+    fail_unless (stop == expect, "%s %uth seek stop is %" GST_TIME_FORMAT
+        ", rather than the expected %" GST_TIME_FORMAT, data->name, num - 1,
+        GST_TIME_ARGS (stop), GST_TIME_ARGS (expect));
+
+  } else if (GST_EVENT_TYPE (event) == GST_EVENT_EOS) {
+    data->num_eos++;
+    fail_unless (data->num_eos <= data->expect_num_eos, "%s received %u "
+        "EOS, more than the expected %u EOS", data->name, data->num_eos,
+        data->expect_num_seeks);
+  }
+
+  return GST_PAD_PROBE_OK;
+}
+
+static void
+_pad_event_data_check_received (PadEventData * data)
+{
+  fail_unless (data->num_eos == data->expect_num_eos, "%s received %u "
+      "EOS, rather than %u", data->num_eos, data->expect_num_eos);
+  fail_unless (data->num_segments == data->expect_num_segments,
+      "%s received %u segments, rather than %u", data->name,
+      data->num_segments, data->expect_num_segments);
+  fail_unless (data->num_seeks == data->expect_num_seeks,
+      "%s received %u seeks, rather than %u", data->name, data->num_seeks,
+      data->expect_num_seeks);
+}
+
+static void
+_pad_event_data_free (PadEventData * data)
+{
+  g_free (data->name);
+  g_array_unref (data->expect_segment_time);
+  g_array_unref (data->expect_seek_start);
+  g_array_unref (data->expect_seek_stop);
+  g_array_unref (data->expect_segment_num_seeks);
+  g_array_unref (data->expect_seek_num_segments);
+  g_free (data);
+}
 
-  pipeline = gst_pipeline_new ("test_pipeline");
+static PadEventData *
+_pad_event_data_new (GstElement * element, const gchar * pad_name,
+    const gchar * suffix)
+{
+  GstPad *pad;
+  PadEventData *data = g_new0 (PadEventData, 1);
+  data->expect_segment_time = g_array_new (FALSE, FALSE, sizeof (GstClockTime));
+  data->expect_seek_start = g_array_new (FALSE, FALSE, sizeof (GstClockTime));
+  data->expect_seek_stop = g_array_new (FALSE, FALSE, sizeof (GstClockTime));
+  data->expect_seek_num_segments = g_array_new (FALSE, FALSE, sizeof (guint));
+  data->expect_segment_num_seeks = g_array_new (FALSE, FALSE, sizeof (guint));
+  data->name = g_strdup_printf ("%s:%s(%s):%s", G_OBJECT_TYPE_NAME (element),
+      GST_ELEMENT_NAME (element), pad_name, suffix);
+
+  pad = gst_element_get_static_pad (element, pad_name);
+  fail_unless (pad, "%s not found", data->name);
+  gst_pad_add_probe (pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM |
+      GST_PAD_PROBE_TYPE_EVENT_UPSTREAM, (GstPadProbeCallback) _test_pad_events,
+      data, (GDestroyNotify) _pad_event_data_free);
+  gst_object_unref (pad);
+
+  return data;
+}
+
+static void
+_pad_event_data_add_expect_segment (PadEventData * data, GstClockTime time)
+{
+  data->expect_num_segments++;
+  g_array_append_val (data->expect_segment_time, time);
+  g_array_append_val (data->expect_segment_num_seeks, data->expect_num_seeks);
+}
+
+static void
+_pad_event_data_add_expect_seek (PadEventData * data, GstClockTime start,
+    GstClockTime stop)
+{
+  data->expect_num_seeks++;
+  g_array_append_val (data->expect_seek_start, start);
+  g_array_append_val (data->expect_seek_stop, stop);
+  g_array_append_val (data->expect_seek_num_segments,
+      data->expect_num_segments);
+}
+
+static void
+_pad_event_data_add_expect_seek_then_segment (PadEventData * data,
+    GstClockTime start, GstClockTime stop)
+{
+  _pad_event_data_add_expect_seek (data, start, stop);
+  _pad_event_data_add_expect_segment (data, start);
+}
+
+#define _EXPECT_SEEK_SEGMENT(data, start, stop) \
+  _pad_event_data_add_expect_seek_then_segment (data, start, stop)
+
+static GstElement *
+_get_source (GstElement * nle_source)
+{
+  GList *tmp;
+  GstElement *bin, *src = NULL;
+
+  fail_unless (g_list_length (GST_BIN_CHILDREN (nle_source)), 1);
+  bin = GST_BIN_CHILDREN (nle_source)->data;
+  fail_unless (GST_IS_BIN (bin));
+
+  for (tmp = GST_BIN_CHILDREN (bin); src == NULL && tmp; tmp = tmp->next) {
+    if (g_strrstr (GST_ELEMENT_NAME (tmp->data), "audiotestsrc"))
+      src = tmp->data;
+  }
+  fail_unless (src);
+  return src;
+}
+
+enum
+{
+  NLE_PREV_SRC,
+  NLE_POST_SRC,
+  NLE_SOURCE_SRC,
+  NLE_OPER_SRC,
+  NLE_OPER_SINK,
+  NLE_IDENTITY_SRC,
+  PREV_SRC,
+  POST_SRC,
+  SOURCE_SRC,
+  PITCH_SRC,
+  PITCH_SINK,
+  IDENTITY_SRC,
+  SINK_SINK,
+  NUM_DATA
+};
+
+static PadEventData **
+_setup_test (GstElement * pipeline, gdouble rate)
+{
+  GstElement *sink, *pitch, *src, *prev, *post, *identity;
+  GstElement *comp, *nle_source, *nle_prev, *nle_post, *nle_oper, *nle_identity;
+  gboolean ret;
+  gchar *suffix;
+  PadEventData **data = g_new0 (PadEventData *, NUM_DATA);
+
+  /* composition */
   comp =
       gst_element_factory_make_or_warn ("nlecomposition", "test_composition");
-
   gst_element_set_state (comp, GST_STATE_READY);
 
+  /* sink */
   sink = gst_element_factory_make_or_warn ("fakesink", "sink");
   gst_bin_add_many (GST_BIN (pipeline), comp, sink, NULL);
 
   gst_element_link (comp, sink);
 
-  /*
-     source1
-     Start : 0s
-     Duration : 2s
-     Priority : 2
-   */
-
-  source1 = audiotest_bin_src ("source1", 0, 2 * GST_SECOND, 2, 2);
-
-  /*
-     def (default source)
-     Priority = G_MAXUINT32
-   */
-  def =
-      audiotest_bin_src ("default", 0 * GST_SECOND, 0 * GST_SECOND, G_MAXUINT32,
-      1);
-  g_object_set (def, "expandable", TRUE, NULL);
-
-  /* Operation */
-  oper = new_operation ("oper", "identity", 0, 2 * GST_SECOND, 1);
-  fail_if (oper == NULL);
-  ((NleObject *) oper)->media_duration_factor = 2.0;
-
-  ASSERT_OBJECT_REFCOUNT (source1, "source1", 1);
-  ASSERT_OBJECT_REFCOUNT (def, "default", 1);
-  ASSERT_OBJECT_REFCOUNT (oper, "oper", 1);
-
-  /* Add source 1 */
-
-  nle_composition_add (GST_BIN (comp), source1);
-  nle_composition_add (GST_BIN (comp), def);
-  nle_composition_add (GST_BIN (comp), oper);
+  /* sources */
+  nle_source =
+      audiotest_bin_src ("nle_source", 3 * GST_SECOND, 4 * GST_SECOND, 3,
+      FALSE);
+  g_object_set (nle_source, "inpoint", 7 * GST_SECOND, NULL);
+  src = _get_source (nle_source);
+  g_object_set (src, "name", "middle-source", NULL);
+
+  nle_prev =
+      audiotest_bin_src ("nle_previous", 0 * GST_SECOND, 3 * GST_SECOND, 2,
+      FALSE);
+  g_object_set (nle_prev, "inpoint", 99 * GST_SECOND, NULL);
+  prev = _get_source (nle_prev);
+  g_object_set (src, "name", "previous-source", NULL);
+
+  nle_post =
+      audiotest_bin_src ("post", 7 * GST_SECOND, 5 * GST_SECOND, 2, FALSE);
+  g_object_set (nle_post, "inpoint", 20 * GST_SECOND, NULL);
+  post = _get_source (nle_post);
+  g_object_set (src, "name", "post-source", NULL);
+
+  /* Operation, must share the same start and duration as the upstream
+   * source */
+  nle_oper =
+      new_operation ("nle_oper", "pitch", 3 * GST_SECOND, 4 * GST_SECOND, 2);
+  fail_unless (g_list_length (GST_BIN_CHILDREN (nle_oper)) == 1);
+  pitch = GST_ELEMENT (GST_BIN_CHILDREN (nle_oper)->data);
+  g_object_set (pitch, "rate", rate, NULL);
+
+  /* cover with an identity operation
+   * rate effect has lower priority, so we don't need the same start or
+   * duration */
+  nle_identity =
+      new_operation ("nle_identity", "identity", 0, 12 * GST_SECOND, 1);
+  g_object_set (nle_identity, "inpoint", 5 * GST_SECOND, NULL);
+  fail_unless (g_list_length (GST_BIN_CHILDREN (nle_oper)) == 1);
+  identity = GST_ELEMENT (GST_BIN_CHILDREN (nle_identity)->data);
+
+  nle_composition_add (GST_BIN (comp), nle_source);
+  nle_composition_add (GST_BIN (comp), nle_prev);
+  nle_composition_add (GST_BIN (comp), nle_post);
+  nle_composition_add (GST_BIN (comp), nle_oper);
+  nle_composition_add (GST_BIN (comp), nle_identity);
+  ret = FALSE;
   commit_and_wait (comp, &ret);
-  check_start_stop_duration (source1, 0, 2 * GST_SECOND, 2 * GST_SECOND);
-  check_start_stop_duration (comp, 0, 2 * GST_SECOND, 2 * GST_SECOND);
-  check_start_stop_duration (oper, 0, 2 * GST_SECOND, 2 * GST_SECOND);
-
-  /* Define expected segments */
-  segments = g_list_append (segments,
-      segment_new (1.0, GST_FORMAT_TIME, 0 * GST_SECOND, 4.0 * GST_SECOND, 0));
-  collect = g_new0 (CollectStructure, 1);
-  collect->comp = comp;
-  collect->sink = sink;
-
-  collect->expected_segments = segments;
-  collect->keep_expected_segments = FALSE;
-
-  sinkpad = gst_element_get_static_pad (sink, "sink");
-  gst_pad_add_probe (sinkpad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM,
-      (GstPadProbeCallback) sinkpad_probe, collect, NULL);
-  gst_object_unref (sinkpad);
-
-  bus = gst_element_get_bus (GST_ELEMENT (pipeline));
-
-  GST_DEBUG ("Setting pipeline to PAUSED");
-  ASSERT_OBJECT_REFCOUNT (source1, "source1", 1);
-
-  fail_if (gst_element_set_state (GST_ELEMENT (pipeline),
-          GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE);
-
-  GST_DEBUG ("Let's poll the bus");
-
-  carry_on = TRUE;
-  while (carry_on) {
-    message = gst_bus_poll (bus, GST_MESSAGE_ANY, GST_SECOND / 10);
-    if (message) {
-      switch (GST_MESSAGE_TYPE (message)) {
-        case GST_MESSAGE_ASYNC_DONE:
-        {
-          carry_on = FALSE;
-          GST_DEBUG ("Pipeline reached PAUSED, stopping polling");
-          break;
-        }
-        case GST_MESSAGE_EOS:
-        {
-          GST_WARNING ("Saw EOS");
+  fail_unless (ret);
+
+  check_start_stop_duration (nle_source, 3 * GST_SECOND, 7 * GST_SECOND,
+      4 * GST_SECOND);
+  check_start_stop_duration (nle_oper, 3 * GST_SECOND, 7 * GST_SECOND,
+      4 * GST_SECOND);
+  check_start_stop_duration (nle_prev, 0, 3 * GST_SECOND, 3 * GST_SECOND);
+  check_start_stop_duration (nle_post, 7 * GST_SECOND, 12 * GST_SECOND,
+      5 * GST_SECOND);
+  check_start_stop_duration (nle_identity, 0, 12 * GST_SECOND, 12 * GST_SECOND);
+  check_start_stop_duration (comp, 0, 12 * GST_SECOND, 12 * GST_SECOND);
+
+  /* create data */
+  suffix = g_strdup_printf ("rate=%g", rate);
+
+  /* source */
+  data[NLE_SOURCE_SRC] = _pad_event_data_new (nle_source, "src", suffix);
+  data[NLE_PREV_SRC] = _pad_event_data_new (nle_prev, "src", suffix);
+  data[NLE_POST_SRC] = _pad_event_data_new (nle_post, "src", suffix);
+  data[SOURCE_SRC] = _pad_event_data_new (src, "src", suffix);
+  data[PREV_SRC] = _pad_event_data_new (prev, "src", suffix);
+  data[POST_SRC] = _pad_event_data_new (post, "src", suffix);
+
+  /* rate operation */
+  data[NLE_OPER_SRC] = _pad_event_data_new (nle_oper, "src", suffix);
+  data[NLE_OPER_SINK] = _pad_event_data_new (nle_oper, "sink", suffix);
+  data[PITCH_SRC] = _pad_event_data_new (pitch, "src", suffix);
+  data[PITCH_SINK] = _pad_event_data_new (pitch, "sink", suffix);
+
+  /* identity: only care about the source pads */
+  data[NLE_IDENTITY_SRC] = _pad_event_data_new (nle_identity, "src", suffix);
+  data[IDENTITY_SRC] = _pad_event_data_new (identity, "src", suffix);
+
+  /* sink */
+  data[SINK_SINK] = _pad_event_data_new (sink, "sink", suffix);
+
+  g_free (suffix);
+
+  return data;
+}
 
-          fail_if (TRUE);
+
+GST_START_TEST (test_tempochange_play)
+{
+  GstElement *pipeline;
+  GstBus *bus;
+  GstMessage *message;
+  gboolean carry_on;
+  PadEventData **data;
+  gdouble rates[3] = { 0.5, 4.0, 1.0 };
+  guint i, j;
+
+  for (i = 0; i < G_N_ELEMENTS (rates); i++) {
+    gdouble rate = rates[i];
+    GST_DEBUG ("rate = %g", rate);
+
+    pipeline = gst_pipeline_new ("test_pipeline");
+
+    data = _setup_test (pipeline, rate);
+
+    /* initial seek */
+    _EXPECT_SEEK_SEGMENT (data[SINK_SINK], 0, 3 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_IDENTITY_SRC], 0, 3 * GST_SECOND);
+    /* nleobject will convert the seek by removing start and adding inpoint */
+    _EXPECT_SEEK_SEGMENT (data[IDENTITY_SRC], 5 * GST_SECOND, 8 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_PREV_SRC], 0, 3 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[PREV_SRC], 99 * GST_SECOND, 102 * GST_SECOND);
+
+    /* rate-stack seek */
+    _EXPECT_SEEK_SEGMENT (data[SINK_SINK], 3 * GST_SECOND, 7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_IDENTITY_SRC], 3 * GST_SECOND,
+        7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[IDENTITY_SRC], 8 * GST_SECOND, 12 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_OPER_SRC], 3 * GST_SECOND, 7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[PITCH_SRC], 0, 4 * GST_SECOND);
+    /* pitch element will change the stop time, e.g. if rate=2.0, then we
+     * want to use up twice as much source, so the stop time doubles */
+    _EXPECT_SEEK_SEGMENT (data[PITCH_SINK], 0, rate * 4 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_OPER_SINK], 3 * GST_SECOND,
+        (GstClockTime) (rate * 4 * GST_SECOND) + 3 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_SOURCE_SRC], 3 * GST_SECOND,
+        (GstClockTime) (rate * 4 * GST_SECOND) + 3 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[SOURCE_SRC], 7 * GST_SECOND,
+        (GstClockTime) (rate * 4 * GST_SECOND) + 7 * GST_SECOND);
+
+    /* final part only involves post source */
+    _EXPECT_SEEK_SEGMENT (data[SINK_SINK], 7 * GST_SECOND, 12 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_IDENTITY_SRC], 7 * GST_SECOND,
+        12 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[IDENTITY_SRC], 12 * GST_SECOND, 17 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_POST_SRC], 7 * GST_SECOND, 12 * GST_SECOND);
+    /* nleobject will convert the seek by removing start and adding
+     * inpoint */
+    _EXPECT_SEEK_SEGMENT (data[POST_SRC], 20 * GST_SECOND, 25 * GST_SECOND);
+
+    /* expect 1 EOS from each, apart from identity, which will get 3 since
+     * part of 3 stacks */
+    for (j = 0; j < NUM_DATA; j++)
+      data[j]->expect_num_eos = 1;
+
+    data[IDENTITY_SRC]->expect_num_eos = 3;
+    data[NLE_IDENTITY_SRC]->expect_num_eos = 3;
+
+    bus = gst_element_get_bus (GST_ELEMENT (pipeline));
+
+    GST_DEBUG ("Setting pipeline to PLAYING");
+    fail_if (gst_element_set_state (GST_ELEMENT (pipeline),
+            GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE);
+
+    GST_DEBUG ("Let's poll the bus");
+
+    carry_on = TRUE;
+    while (carry_on) {
+      message = gst_bus_poll (bus, GST_MESSAGE_ANY, GST_SECOND / 10);
+      if (message) {
+        switch (GST_MESSAGE_TYPE (message)) {
+          case GST_MESSAGE_EOS:
+            if (message->src == GST_OBJECT (pipeline)) {
+              GST_DEBUG ("Setting pipeline to NULL");
+              fail_unless (gst_element_set_state (GST_ELEMENT (pipeline),
+                      GST_STATE_NULL) == GST_STATE_CHANGE_SUCCESS);
+              carry_on = FALSE;
+            }
+            break;
+          case GST_MESSAGE_ERROR:
+            fail_error_message (message);
+            break;
+          default:
+            break;
         }
-        case GST_MESSAGE_ERROR:
-          fail_error_message (message);
-        default:
-          break;
+        gst_message_unref (message);
       }
-      gst_mini_object_unref (GST_MINI_OBJECT (message));
     }
-  }
-
-  fail_unless_equals_float (((NleObject *) source1)->media_duration_factor,
-      1.0f);
-  fail_unless_equals_float (((NleObject *)
-          source1)->recursive_media_duration_factor, 2.0f);
-  fail_unless_equals_float (((NleObject *) oper)->media_duration_factor, 2.0f);
-  fail_unless_equals_float (((NleObject *)
-          oper)->recursive_media_duration_factor, 2.0f);
 
-  GST_DEBUG ("Setting pipeline to READY");
+    for (j = 0; j < NUM_DATA; j++)
+      _pad_event_data_check_received (data[j]);
 
-  fail_if (gst_element_set_state (GST_ELEMENT (pipeline),
-          GST_STATE_READY) == GST_STATE_CHANGE_FAILURE);
-
-  fail_if (collect->expected_segments != NULL);
+    ASSERT_OBJECT_REFCOUNT_BETWEEN (pipeline, "main pipeline", 1, 2);
+    gst_object_unref (pipeline);
+    ASSERT_OBJECT_REFCOUNT_BETWEEN (bus, "main bus", 1, 2);
+    gst_object_unref (bus);
+    g_free (data);
+  }
+}
 
-  fail_if (gst_element_set_state (GST_ELEMENT (pipeline),
-          GST_STATE_NULL) == GST_STATE_CHANGE_FAILURE);
+GST_END_TEST;
 
-  ASSERT_OBJECT_REFCOUNT_BETWEEN (pipeline, "main pipeline", 1, 2);
-  gst_object_unref (pipeline);
-  ASSERT_OBJECT_REFCOUNT_BETWEEN (bus, "main bus", 1, 2);
-  gst_object_unref (bus);
+#define _WAIT_UNTIL_ASYNC_DONE \
+{ \
+  GST_DEBUG ("Let's poll the bus"); \
+  carry_on = TRUE; \
+  while (carry_on) { \
+    message = gst_bus_poll (bus, GST_MESSAGE_ANY, GST_SECOND / 10); \
+    if (message) { \
+      switch (GST_MESSAGE_TYPE (message)) { \
+        case GST_MESSAGE_EOS: \
+          fail_if (TRUE, "Received EOS"); \
+          break; \
+        case GST_MESSAGE_ERROR: \
+          fail_error_message (message); \
+          break; \
+        case GST_MESSAGE_ASYNC_DONE: \
+          carry_on = FALSE; \
+          break; \
+        default: \
+          break; \
+      } \
+      gst_message_unref (message); \
+    } \
+  } \
+}
 
-  collect_free (collect);
+GST_START_TEST (test_tempochange_seek)
+{
+  GstElement *pipeline;
+  GstBus *bus;
+  GstMessage *message;
+  gboolean carry_on;
+  PadEventData **data;
+  gdouble rates[3] = { 2.0, 0.25, 1.0 };
+  guint i, j;
+  GstClockTime offset = 0.1 * GST_SECOND;
+
+  for (i = 0; i < G_N_ELEMENTS (rates); i++) {
+    gdouble rate = rates[i];
+    GST_DEBUG ("rate = %g", rate);
+
+    pipeline = gst_pipeline_new ("test_pipeline");
+
+    data = _setup_test (pipeline, rate);
+
+    /* initial seek from the pause */
+    _EXPECT_SEEK_SEGMENT (data[SINK_SINK], 0, 3 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_IDENTITY_SRC], 0, 3 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[IDENTITY_SRC], 5 * GST_SECOND, 8 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_PREV_SRC], 0, 3 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[PREV_SRC], 99 * GST_SECOND, 102 * GST_SECOND);
+
+    GST_DEBUG ("Setting pipeline to PAUSED");
+    fail_unless (gst_element_set_state (GST_ELEMENT (pipeline),
+            GST_STATE_PAUSED) == GST_STATE_CHANGE_ASYNC);
+
+    bus = gst_element_get_bus (GST_ELEMENT (pipeline));
+
+    _WAIT_UNTIL_ASYNC_DONE;
+
+    for (j = 0; j < NUM_DATA; j++)
+      _pad_event_data_check_received (data[j]);
+
+    /* first seek for just after the start of the rate effect */
+    /* NOTE: neither prev nor post should receive anything */
+
+    /* sink will receive two seeks: one that initiates the pre-roll, and
+     * then the seek with the stop set */
+    /* expect no segment for the first seek */
+    _pad_event_data_add_expect_seek (data[SINK_SINK], 3 * GST_SECOND + offset,
+        GST_CLOCK_TIME_NONE);
+    _EXPECT_SEEK_SEGMENT (data[SINK_SINK], 3 * GST_SECOND + offset,
+        7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_IDENTITY_SRC], 3 * GST_SECOND + offset,
+        7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[IDENTITY_SRC], 8 * GST_SECOND + offset,
+        12 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_OPER_SRC], 3 * GST_SECOND + offset,
+        7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[PITCH_SRC], offset, 4 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[PITCH_SINK], rate * offset,
+        rate * 4 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_OPER_SINK],
+        3 * GST_SECOND + (GstClockTime) (rate * offset),
+        3 * GST_SECOND + (GstClockTime) (rate * 4 * GST_SECOND));
+    _EXPECT_SEEK_SEGMENT (data[NLE_SOURCE_SRC],
+        3 * GST_SECOND + (GstClockTime) (rate * offset),
+        3 * GST_SECOND + (GstClockTime) (rate * 4 * GST_SECOND));
+    _EXPECT_SEEK_SEGMENT (data[SOURCE_SRC],
+        7 * GST_SECOND + (GstClockTime) (rate * offset),
+        7 * GST_SECOND + (GstClockTime) (rate * 4 * GST_SECOND));
+
+    /* perform seek */
+    fail_unless (gst_element_seek_simple (pipeline, GST_FORMAT_TIME,
+            GST_SEEK_FLAG_FLUSH, 3 * GST_SECOND + offset));
+
+    _WAIT_UNTIL_ASYNC_DONE;
+
+    for (j = 0; j < NUM_DATA; j++)
+      _pad_event_data_check_received (data[j]);
+
+    /* now seek to just before the end */
+    _pad_event_data_add_expect_seek (data[SINK_SINK], 7 * GST_SECOND - offset,
+        GST_CLOCK_TIME_NONE);
+    _EXPECT_SEEK_SEGMENT (data[SINK_SINK], 7 * GST_SECOND - offset,
+        7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_IDENTITY_SRC], 7 * GST_SECOND - offset,
+        7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[IDENTITY_SRC], 12 * GST_SECOND - offset,
+        12 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_OPER_SRC], 7 * GST_SECOND - offset,
+        7 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[PITCH_SRC], 4 * GST_SECOND - offset,
+        4 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[PITCH_SINK],
+        rate * (4 * GST_SECOND) - rate * offset, rate * 4 * GST_SECOND);
+    _EXPECT_SEEK_SEGMENT (data[NLE_OPER_SINK],
+        3 * GST_SECOND + (GstClockTime) (rate * (4 * GST_SECOND - offset)),
+        3 * GST_SECOND + (GstClockTime) (rate * 4 * GST_SECOND));
+    _EXPECT_SEEK_SEGMENT (data[NLE_SOURCE_SRC],
+        3 * GST_SECOND + (GstClockTime) (rate * (4 * GST_SECOND - offset)),
+        3 * GST_SECOND + (GstClockTime) (rate * 4 * GST_SECOND));
+    _EXPECT_SEEK_SEGMENT (data[SOURCE_SRC],
+        7 * GST_SECOND + (GstClockTime) (rate * (4 * GST_SECOND - offset)),
+        7 * GST_SECOND + (GstClockTime) (rate * 4 * GST_SECOND));
+
+    /* perform seek */
+    fail_unless (gst_element_seek_simple (pipeline, GST_FORMAT_TIME,
+            GST_SEEK_FLAG_FLUSH, 7 * GST_SECOND - offset));
+
+    _WAIT_UNTIL_ASYNC_DONE;
+
+    for (j = 0; j < NUM_DATA; j++)
+      _pad_event_data_check_received (data[j]);
+
+    GST_DEBUG ("Setting pipeline to NULL");
+    fail_unless (gst_element_set_state (GST_ELEMENT (pipeline),
+            GST_STATE_NULL) == GST_STATE_CHANGE_SUCCESS);
+
+    ASSERT_OBJECT_REFCOUNT_BETWEEN (pipeline, "main pipeline", 1, 2);
+    gst_object_unref (pipeline);
+    ASSERT_OBJECT_REFCOUNT_BETWEEN (bus, "main bus", 1, 2);
+    gst_object_unref (bus);
+    g_free (data);
+  }
 }
 
 GST_END_TEST;
@@ -171,7 +641,8 @@ gnonlin_suite (void)
   ges_init ();
   suite_add_tcase (s, tc_chain);
 
-  tcase_add_test (tc_chain, test_tempochange);
+  tcase_add_test (tc_chain, test_tempochange_play);
+  tcase_add_test (tc_chain, test_tempochange_seek);
 
   return s;
 }