Handle changing playback rate
authorSjors Gielen <sjors@sjorsgielen.nl>
Sun, 20 Dec 2015 13:03:57 +0000 (14:03 +0100)
committerThibault Saunier <tsaunier@gnome.org>
Fri, 26 Feb 2016 18:54:40 +0000 (19:54 +0100)
Before this patch, NLE and GES did not support NleOperations (respectively
GESEffects) that changed the speed/tempo/rate at which the source plays. For
example, the 'pitch' element can make audio play faster or slower. In GES 1.5.90
and before, an NleOperation containing the pitch element to change the rate (or
tempo) would cause a pipeline state change to PAUSED after that stack; that has
been fixed in 1.5.91 (see #755012 [0]). But even then, in 1.5.91 and later,
NleComposition would send segment events to its NleSources assuming that one
source second is equal to one pipeline second. The resulting early EOS event
(in the case of a source rate higher than 1.0) would cause it to switch stacks
too early, causing confusion in the timeline and spectacularly messed up
output.

This patch fixes that by searching for rate-changing elements in
GESTrackElements such as GESEffects. If such rate-changing elements are found,
their final effect on the playing rate is stored in the corresponding NleObject
as the 'media duration factor', named like this because the 'media duration',
or source duration, of an NleObject can be computed by multiplying the duration
with the media duration factor of that object and its parents (this is called
the 'recursive media duration factor'). For example, a 4-second NleSource with
an NleOperation with a media duration factor of 2.0 will have an 8-second media
duration, which means that for playing 4 seconds in the pipeline, the seek
event sent to it must span 8 seconds of media. (So, the 'duration' of an
NleObject or GES object always refers to its duration in the timeline, not the
media duration.)

To summarize:

* Rate-changing elements are registered in the GESEffectClass (pitch::tempo and
  pitch::rate are registered by default);
* GESTimelineElement is responsible for detecting rate-changing elements and
  computing the media_duration_factor;
* GESTrackElement is responsible for storing the media_duration_factor in
  NleObject;
* NleComposition is responsible for the recursive_media_duration_factor;
* The latter property finally fixes media time computations in NleObject.

NLE and GES tests are included.

[0] https://bugzilla.gnome.org/show_bug.cgi?id=755012

Differential Revision: https://phabricator.freedesktop.org/D276

13 files changed:
.gitignore
ges/ges-clip.c
ges/ges-effect.c
ges/ges-effect.h
ges/ges-internal.h
ges/ges-timeline-element.c
ges/ges-track-element.c
plugins/nle/nlecomposition.c
plugins/nle/nleobject.c
plugins/nle/nleobject.h
tests/check/Makefile.am
tests/check/ges/tempochange.c [new file with mode: 0644]
tests/check/nle/tempochange.c [new file with mode: 0644]

index 1b158eb..42d6e30 100644 (file)
@@ -68,6 +68,7 @@ log
 /tests/check/ges/mixers
 /tests/check/ges/timelineedition
 /tests/check/ges/timelinegroup
+/tests/check/ges/tempochange
 /tests/check/ges/uriclip
 /tests/check/integration
 /tests/benchmarks/timeline
@@ -78,6 +79,7 @@ log
 /tests/check/nle/nlecomposition
 /tests/check/nle/nleoperation
 /tests/check/nle/nlesource
+/tests/check/nle/tempochange
 
 /tools/ges-launch-1.0
 /tests/check/ges/project
index 3dbb2ac..12df14d 100644 (file)
@@ -1246,13 +1246,24 @@ ges_clip_set_top_effect_index (GESClip * clip, GESBaseEffect * effect,
 /**
  * ges_clip_split:
  * @clip: the #GESClip to split
- * @position: a #GstClockTime representing the position at which to split
+ * @position: a #GstClockTime representing the timeline position at which to split
  *
- * The function modifies @clip, and creates another #GESClip so
- * we have two clips at the end, splitted at the time specified by @position.
- * The newly created clip will be added to the same layer as @clip is in.
- * This implies that @clip must be in a #GESLayer for the operation to
- * be possible.
+ * The function modifies @clip, and creates another #GESClip so we have two
+ * clips at the end, splitted at the time specified by @position, as a position
+ * in the timeline (not in the clip to be split). For example, if
+ * ges_clip_split is called on a 4-second clip playing from 0:01.00 until
+ * 0:05.00, with a split position of 0:02.00, this will result in one clip of 1
+ * second and one clip of 3 seconds, not in two clips of 2 seconds.
+ *
+ * The newly created clip will be added to the same layer as @clip is in. This
+ * implies that @clip must be in a #GESLayer for the operation to be possible.
+ *
+ * This method supports clips playing at a different tempo than one second per
+ * second. For example, splitting a clip with a #GESEffect 'pitch tempo=1.5'
+ * four seconds after it starts, will set the inpoint of the new clip to six
+ * seconds after that of the clip to split. For this, the rate-changing
+ * property must be registered using @ges_effect_class_register_rate_property;
+ * for the 'pitch' plugin, this is already done.
  *
  * Returns: (transfer none): The newly created #GESClip resulting from the
  * splitting
@@ -1262,7 +1273,8 @@ ges_clip_split (GESClip * clip, guint64 position)
 {
   GList *tmp;
   GESClip *new_object;
-  GstClockTime start, inpoint, duration;
+  GstClockTime start, inpoint, duration, old_duration, new_duration;
+  gdouble media_duration_factor;
 
   g_return_val_if_fail (GES_IS_CLIP (clip), NULL);
   g_return_val_if_fail (clip->priv->layer, NULL);
@@ -1287,11 +1299,15 @@ ges_clip_split (GESClip * clip, guint64 position)
 
   GST_DEBUG_OBJECT (new_object, "New 'splitted' clip");
   /* Set new timing properties on the Clip */
+  media_duration_factor =
+      ges_timeline_element_get_media_duration_factor (GES_TIMELINE_ELEMENT
+      (clip));
+  new_duration = duration + start - position;
+  old_duration = position - start;
   _set_start0 (GES_TIMELINE_ELEMENT (new_object), position);
   _set_inpoint0 (GES_TIMELINE_ELEMENT (new_object),
-      _INPOINT (clip) + duration - (duration + start - position));
-  _set_duration0 (GES_TIMELINE_ELEMENT (new_object),
-      duration + start - position);
+      inpoint + old_duration * media_duration_factor);
+  _set_duration0 (GES_TIMELINE_ELEMENT (new_object), new_duration);
 
   /* We do not want the timeline to create again TrackElement-s */
   ges_clip_set_moving_from_layer (new_object, TRUE);
@@ -1313,9 +1329,8 @@ ges_clip_split (GESClip * clip, guint64 position)
     /* Set 'new' track element timing propeties */
     _set_start0 (GES_TIMELINE_ELEMENT (new_trackelement), position);
     _set_inpoint0 (GES_TIMELINE_ELEMENT (new_trackelement),
-        inpoint + duration - (duration + start - position));
-    _set_duration0 (GES_TIMELINE_ELEMENT (new_trackelement),
-        duration + start - position);
+        inpoint + old_duration * media_duration_factor);
+    _set_duration0 (GES_TIMELINE_ELEMENT (new_trackelement), new_duration);
 
     ges_container_add (GES_CONTAINER (new_object),
         GES_TIMELINE_ELEMENT (new_trackelement));
@@ -1326,7 +1341,7 @@ ges_clip_split (GESClip * clip, guint64 position)
         position - start + inpoint);
   }
 
-  _set_duration0 (GES_TIMELINE_ELEMENT (clip), position - _START (clip));
+  _set_duration0 (GES_TIMELINE_ELEMENT (clip), old_duration);
 
   return new_object;
 }
index d20b661..770bf1f 100644 (file)
@@ -159,6 +159,10 @@ ges_effect_class_init (GESEffectClass * klass)
 
   obj_bg_class->create_element = ges_effect_create_element;
 
+  klass->rate_properties = NULL;
+  ges_effect_class_register_rate_property (klass, "pitch", "tempo");
+  ges_effect_class_register_rate_property (klass, "pitch", "rate");
+
   /**
    * GESEffect:bin-description:
    *
@@ -270,3 +274,100 @@ ges_effect_new (const gchar * bin_description)
 
   return effect;
 }
+
+static int
+property_name_compare (gconstpointer s1, gconstpointer s2)
+{
+  return g_strcmp0 ((const gchar *) s1, (const gchar *) s2);
+}
+
+/**
+ * ges_effect_class_register_rate_property:
+ * @klass: Instance of the GESEffectClass
+ * @element_name: Name of the GstElement that changes the rate
+ * @property_name: Name of the property that changes the rate
+ *
+ * Register an element that can change the rate at which media is playing. The
+ * property type must be float or double, and must be a factor of the rate,
+ * i.e. a value of 2.0 must mean that the media plays twice as fast. For
+ * example, this is true for the properties 'rate' and 'tempo' of the element
+ * 'pitch', which is already registered by default. By registering the element,
+ * timeline duration can be correctly converted into media duration, allowing
+ * the right segment seeks to be sent to the sources.
+ *
+ * A reference to the GESEffectClass can be obtained as follows:
+ *   GES_EFFECT_CLASS (g_type_class_ref (GES_TYPE_EFFECT));
+ *
+ * Returns: whether the rate property was succesfully registered. When this
+ * method returns false, a warning is emitted with more information.
+ */
+gboolean
+ges_effect_class_register_rate_property (GESEffectClass * klass,
+    const gchar * element_name, const gchar * property_name)
+{
+  GstElementFactory *element_factory = NULL;
+  GstElement *element = NULL;
+  GParamSpec *pspec = NULL;
+  gchar *full_property_name = NULL;
+  GType param_type;
+  gboolean res = FALSE;
+
+  element_factory = gst_element_factory_find (element_name);
+  if (element_factory == NULL) {
+    GST_WARNING
+        ("Did not add rate property '%s' for element '%s': the element factory could not be found",
+        property_name, element_name);
+    goto fail;
+  }
+
+  element = gst_element_factory_create (element_factory, NULL);
+  if (element == NULL) {
+    GST_WARNING
+        ("Did not add rate property '%s' for element '%s': the element could not be constructed",
+        property_name, element_name);
+    goto fail;
+  }
+
+  pspec =
+      g_object_class_find_property (G_OBJECT_GET_CLASS (element),
+      property_name);
+  if (pspec == NULL) {
+    GST_WARNING
+        ("Did not add rate property '%s' for element '%s': the element did not have the property name specified",
+        property_name, element_name);
+    goto fail;
+  }
+
+  param_type = G_PARAM_SPEC_VALUE_TYPE (pspec);
+  if (param_type != G_TYPE_FLOAT && param_type != G_TYPE_DOUBLE) {
+    GST_WARNING
+        ("Did not add rate property '%s' for element '%s': the property is not of float or double type",
+        property_name, element_name);
+    goto fail;
+  }
+
+  full_property_name = g_strdup_printf ("%s::%s",
+      g_type_name (gst_element_factory_get_element_type (element_factory)),
+      property_name);
+
+  if (g_list_find_custom (klass->rate_properties, full_property_name,
+          property_name_compare) == NULL) {
+    klass->rate_properties =
+        g_list_append (klass->rate_properties, full_property_name);
+    GST_DEBUG ("Added rate property %s", full_property_name);
+  } else {
+    g_free (full_property_name);
+  }
+
+  res = TRUE;
+
+fail:
+  if (element_factory != NULL)
+    gst_object_unref (element_factory);
+  if (element != NULL)
+    gst_object_unref (element);
+  if (pspec != NULL)
+    g_param_spec_unref (pspec);
+
+  return res;
+}
index 34cf5d4..bc3c480 100644 (file)
@@ -68,6 +68,9 @@ struct _GESEffectClass
 {
   /*< private > */
   GESBaseEffectClass parent_class;
+
+  GList *rate_properties;
+
   /* Padding for API extension */
   gpointer _ges_reserved[GES_PADDING];
 
@@ -78,5 +81,8 @@ GType ges_effect_get_type (void);
 GESEffect*
 ges_effect_new (const gchar * bin_description);
 
+gboolean
+ges_effect_class_register_rate_property (GESEffectClass *klass, const gchar *element, const gchar *property_name);
+
 G_END_DECLS
 #endif /* _GES_EFFECT */
index 8760ad5..37f29aa 100644 (file)
@@ -364,6 +364,11 @@ G_GNUC_INTERNAL GESImageSource     * ges_image_source_new      (gchar *uri);
 G_GNUC_INTERNAL GESTitleSource     * ges_title_source_new      (void);
 G_GNUC_INTERNAL GESVideoTestSource * ges_video_test_source_new (void);
 
+/****************************************************
+ *              GESTimelineElement                  *
+ ****************************************************/
+G_GNUC_INTERNAL gdouble ges_timeline_element_get_media_duration_factor(GESTimelineElement *self);
+
 /******************************
  *  GESMultiFile internal API *
  ******************************/
index 09d5d89..7abc297 100644 (file)
@@ -31,6 +31,7 @@
 #include "ges-extractable.h"
 #include "ges-meta-container.h"
 #include "ges-internal.h"
+#include "ges-effect.h"
 
 #include <string.h>
 #include <gobject/gvaluecollector.h>
@@ -1727,3 +1728,47 @@ ges_timeline_element_paste (GESTimelineElement * self,
 
   return g_object_ref (res);
 }
+
+/* Internal */
+gdouble
+ges_timeline_element_get_media_duration_factor (GESTimelineElement * self)
+{
+  gdouble media_duration_factor;
+  GESEffectClass *class;
+  GList *props;
+
+  media_duration_factor = 1.0;
+
+  class = GES_EFFECT_CLASS (g_type_class_ref (GES_TYPE_EFFECT));
+
+  for (props = class->rate_properties; props != NULL; props = props->next) {
+    GObject *child;
+    GParamSpec *pspec;
+    if (ges_timeline_element_lookup_child (self, props->data, &child, &pspec)) {
+      if (G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_FLOAT) {
+        gfloat rate_change;
+        g_object_get (child, pspec->name, &rate_change, NULL);
+        media_duration_factor *= rate_change;
+      } else if (G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_DOUBLE) {
+        gdouble rate_change;
+        g_object_get (child, pspec->name, &rate_change, NULL);
+        media_duration_factor *= rate_change;
+      } else {
+        GST_WARNING_OBJECT (self,
+            "Rate property %s in child %" GST_PTR_FORMAT
+            " is of unsupported type %s", pspec->name, child,
+            G_VALUE_TYPE_NAME (pspec->value_type));
+      }
+
+      gst_object_unref (child);
+      g_param_spec_unref (pspec);
+
+      GST_DEBUG_OBJECT (self,
+          "Added rate changing property %s, set to value %lf",
+          (const char *) props->data, media_duration_factor);
+    }
+  }
+
+  g_type_class_unref (class);
+  return media_duration_factor;
+}
index 5a23071..d6a3380 100644 (file)
@@ -194,6 +194,7 @@ ges_track_element_constructed (GObject * gobject)
 {
   GESTrackElementClass *class;
   GstElement *nleobject;
+  gdouble media_duration_factor;
   gchar *tmp;
   GESTrackElement *object = GES_TRACK_ELEMENT (gobject);
 
@@ -228,6 +229,12 @@ ges_track_element_constructed (GObject * gobject)
       "priority", GES_TIMELINE_ELEMENT_PRIORITY (object),
       "active", object->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);
+
   G_OBJECT_CLASS (ges_track_element_parent_class)->constructed (gobject);
 }
 
index 963ea69..bc60650 100644 (file)
@@ -2562,6 +2562,7 @@ _relink_single_node (NleComposition * comp, GNode * node,
 {
   NleObject *newobj;
   NleObject *newparent;
+  GNode *node_it;
   GstPad *srcpad = NULL, *sinkpad = NULL;
   GstEvent *translated_seek;
 
@@ -2574,6 +2575,12 @@ _relink_single_node (NleComposition * comp, GNode * node,
   GST_DEBUG_OBJECT (comp, "newobj:%s",
       GST_ELEMENT_NAME ((GstElement *) newobj));
 
+  newobj->recursive_media_duration_factor = 1.0f;
+  for (node_it = node; node_it != NULL; node_it = node_it->parent) {
+    NleObject *object = (NleObject *) node_it->data;
+    newobj->recursive_media_duration_factor *= object->media_duration_factor;
+  }
+
   srcpad = NLE_OBJECT_SRC (newobj);
 
   gst_bin_add (GST_BIN (comp->priv->current_bin), GST_ELEMENT (newobj));
index e8db1db..46694a4 100644 (file)
@@ -77,6 +77,7 @@ enum
   PROP_ACTIVE,
   PROP_CAPS,
   PROP_EXPANDABLE,
+  PROP_MEDIA_DURATION_FACTOR,
   PROP_LAST
 };
 
@@ -246,6 +247,21 @@ nle_object_class_init (NleObjectClass * klass)
       properties[PROP_EXPANDABLE]);
 
   /**
+   * NleObject:media-duration-factor
+   *
+   * Indicates the relative rate caused by this object, in other words, the
+   * 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.
+   */
+  properties[PROP_MEDIA_DURATION_FACTOR] =
+      g_param_spec_double ("media-duration-factor", "Media duration factor",
+      "The relative rate caused by this object", 0.01, G_MAXDOUBLE,
+      1.0, G_PARAM_READWRITE);
+  g_object_class_install_property (gobject_class, PROP_MEDIA_DURATION_FACTOR,
+      properties[PROP_MEDIA_DURATION_FACTOR]);
+
+  /**
    * NleObject::commit
    * @object: a #NleObject
    * @recurse: Whether to commit recursiverly into (NleComposition) children of
@@ -281,6 +297,8 @@ 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,
@@ -342,17 +360,23 @@ nle_object_to_media_time (NleObject * object, GstClockTime otime,
   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;
+      *mtime =
+          object->inpoint +
+          object->duration * object->recursive_media_duration_factor;
     else
-      *mtime = object->stop - object->start;
+      *mtime =
+          (object->stop -
+          object->start) * object->recursive_media_duration_factor;
     return FALSE;
   }
 
   if (G_UNLIKELY (object->inpoint == GST_CLOCK_TIME_NONE)) {
     /* no time shifting, for live sources ? */
-    *mtime = otime - object->start;
+    *mtime = (otime - object->start) * object->recursive_media_duration_factor;
   } else {
-    *mtime = otime - object->start + object->inpoint;
+    *mtime =
+        (otime - object->start) * object->recursive_media_duration_factor +
+        object->inpoint;
   }
 
   GST_DEBUG_OBJECT (object, "Returning MediaTime : %" GST_TIME_FORMAT,
@@ -543,6 +567,9 @@ nle_object_set_property (GObject * object, guint prop_id,
       else
         GST_OBJECT_FLAG_UNSET (nleobject, NLE_OBJECT_EXPANDABLE);
       break;
+    case PROP_MEDIA_DURATION_FACTOR:
+      nleobject->media_duration_factor = g_value_get_double (value);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
@@ -581,6 +608,9 @@ nle_object_get_property (GObject * object, guint prop_id,
     case PROP_EXPANDABLE:
       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);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
       break;
index 1e0272e..5af9717 100644 (file)
@@ -126,6 +126,12 @@ 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 40a31c4..345db5c 100644 (file)
@@ -51,10 +51,12 @@ check_PROGRAMS = \
        ges/group\
        ges/project\
        ges/track\
+       ges/tempochange \
        nle/simple      \
        nle/complex     \
        nle/nleoperation        \
-       nle/nlecomposition
+       nle/nlecomposition      \
+       nle/tempochange
 
 # We do not support sources outside composition
 #      nle/nlesource
diff --git a/tests/check/ges/tempochange.c b/tests/check/ges/tempochange.c
new file mode 100644 (file)
index 0000000..42b48ec
--- /dev/null
@@ -0,0 +1,147 @@
+/* GStreamer Editing Services
+ * Copyright (C) 2016 Sjors Gielen <mixml-ges@sjorsgielen.nl>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "test-utils.h"
+#include <ges/ges.h>
+#include <gst/check/gstcheck.h>
+#include <plugins/nle/nleobject.h>
+
+GST_START_TEST (test_tempochange)
+{
+  GESTimeline *timeline;
+  GESLayer *layer;
+  GESTrack *track_audio;
+  GESEffect *effect;
+  GESTestClip *clip;
+  GESClip *clip2, *clip3;
+  GList *tmp;
+  int found;
+
+  ges_init ();
+
+  timeline = ges_timeline_new ();
+  layer = ges_layer_new ();
+  track_audio = GES_TRACK (ges_audio_track_new ());
+
+  ges_timeline_add_track (timeline, track_audio);
+  ges_timeline_add_layer (timeline, layer);
+
+  /* Add a 9-second clip */
+  clip = ges_test_clip_new ();
+  g_object_set (clip, "duration", 9 * GST_SECOND, NULL);
+  ges_layer_add_clip (layer, (GESClip *) clip);
+
+  /* Split it after 3 seconds */
+  clip2 = ges_clip_split ((GESClip *) clip, 3 * GST_SECOND);
+
+  /* Add pitch effect to play 1.5 times faster */
+  effect = ges_effect_new ("pitch tempo=1.5");
+
+  fail_unless (GES_IS_EFFECT (effect));
+  fail_unless (ges_container_add (GES_CONTAINER (clip2),
+          GES_TIMELINE_ELEMENT (effect)));
+  fail_unless (ges_track_element_get_track (GES_TRACK_ELEMENT (effect)) !=
+      NULL);
+
+  assert_equals_int (GES_TRACK_ELEMENT (effect)->active, TRUE);
+
+  /* Split clip again after 6 seconds (note: this is timeline time) */
+  clip3 = ges_clip_split (clip2, 6 * GST_SECOND);
+
+  // note: start and duration are counted in timeline time, while
+  // inpoint is counted in media time.
+  fail_unless_equals_int64 (_START (clip), 0 * GST_SECOND);
+  fail_unless_equals_int64 (_INPOINT (clip), 0 * GST_SECOND);
+  fail_unless_equals_int64 (_DURATION (clip), 3 * GST_SECOND);
+
+  fail_unless_equals_int64 (_START (clip2), 3 * GST_SECOND);
+  fail_unless_equals_int64 (_INPOINT (clip2), 3 * GST_SECOND);
+  fail_unless_equals_int64 (_DURATION (clip2), 3 * GST_SECOND);
+
+  fail_unless_equals_int64 (_START (clip3), 6 * GST_SECOND);
+  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);
+
+  gst_object_unref (timeline);
+}
+
+GST_END_TEST;
+
+static Suite *
+ges_suite (void)
+{
+  Suite *s = suite_create ("ges");
+  TCase *tc_chain = tcase_create ("tempochange");
+  GstPluginFeature *pitch = gst_registry_find_feature (gst_registry_get (),
+      "pitch", GST_TYPE_ELEMENT_FACTORY);
+
+  if (pitch) {
+    gst_object_unref (pitch);
+
+    suite_add_tcase (s, tc_chain);
+    tcase_add_test (tc_chain, test_tempochange);
+  }
+
+  return s;
+}
+
+GST_CHECK_MAIN (ges);
diff --git a/tests/check/nle/tempochange.c b/tests/check/nle/tempochange.c
new file mode 100644 (file)
index 0000000..a313c90
--- /dev/null
@@ -0,0 +1,171 @@
+/* GStreamer Editing Services
+ * Copyright (C) 2016 Sjors Gielen <mixml-ges@sjorsgielen.nl>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "common.h"
+#include "plugins/nle/nleobject.h"
+
+GST_START_TEST (test_tempochange)
+{
+  GstElement *pipeline;
+  GstElement *comp, *source1, *def, *sink, *oper;
+  GList *segments = NULL;
+  GstBus *bus;
+  GstMessage *message;
+  gboolean carry_on, ret = FALSE;
+  CollectStructure *collect;
+  GstPad *sinkpad;
+
+  pipeline = gst_pipeline_new ("test_pipeline");
+  comp =
+      gst_element_factory_make_or_warn ("nlecomposition", "test_composition");
+
+  gst_element_set_state (comp, GST_STATE_READY);
+
+  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);
+  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);
+
+  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_if (TRUE);
+        }
+        case GST_MESSAGE_ERROR:
+          fail_error_message (message);
+        default:
+          break;
+      }
+      gst_mini_object_unref (GST_MINI_OBJECT (message));
+    }
+  }
+
+  fail_if (((NleObject *) source1)->media_duration_factor != 1.0f);
+  fail_if (((NleObject *) source1)->recursive_media_duration_factor != 2.0f);
+  fail_if (((NleObject *) oper)->media_duration_factor != 2.0f);
+  fail_if (((NleObject *) oper)->recursive_media_duration_factor != 2.0f);
+
+  GST_DEBUG ("Setting pipeline to READY");
+
+  fail_if (gst_element_set_state (GST_ELEMENT (pipeline),
+          GST_STATE_READY) == GST_STATE_CHANGE_FAILURE);
+
+  fail_if (collect->expected_segments != NULL);
+
+  fail_if (gst_element_set_state (GST_ELEMENT (pipeline),
+          GST_STATE_NULL) == GST_STATE_CHANGE_FAILURE);
+
+  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 (collect);
+}
+
+GST_END_TEST;
+
+static Suite *
+gnonlin_suite (void)
+{
+  Suite *s = suite_create ("nle");
+  TCase *tc_chain = tcase_create ("tempochange");
+
+  ges_init ();
+  suite_add_tcase (s, tc_chain);
+
+  tcase_add_test (tc_chain, test_tempochange);
+
+  return s;
+}
+
+GST_CHECK_MAIN (gnonlin)