clip: add methods to convert between time coordinates
authorHenry Wilkes <hwilkes@igalia.com>
Tue, 12 May 2020 17:18:09 +0000 (18:18 +0100)
committerHenry Wilkes <hwilkes@igalia.com>
Mon, 25 May 2020 10:20:38 +0000 (11:20 +0100)
Add methods to convert between the timeline time coordinates and the
internal time coordinates of a track element in a clip, taking time
effects into account.

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

ges/ges-clip.c
ges/ges-clip.h
ges/ges-timeline-element.c
tests/check/ges/clip.c

index baf42b4466eb998d2b3388060ac346465c00c842..f04ba10481466dc92fdbd15bf6d86ceab60060a4 100644 (file)
 static GList *ges_clip_create_track_elements_func (GESClip * clip,
     GESTrackType type);
 static void _compute_height (GESContainer * container);
+static GstClockTime _convert_core_time (GESClip * clip, GstClockTime time,
+    gboolean to_timeline, gboolean * no_core, GError ** error);
 
 struct _GESClipPrivate
 {
@@ -286,7 +288,8 @@ _duration_limit_data_list_with_data (GESClip * clip, DurationLimitData * data)
 }
 
 static gint
-_cmp_by_track_then_priority (gconstpointer a_p, gconstpointer b_p)
+_cmp_duration_limit_data_by_track_then_priority (gconstpointer a_p,
+    gconstpointer b_p)
 {
   const DurationLimitData *a = a_p, *b = b_p;
   if (a->track < b->track)
@@ -408,7 +411,8 @@ _calculate_duration_limit (GESClip * self, GList * child_data)
   GstClockTime limit = GST_CLOCK_TIME_NONE;
   GList *start, *end;
 
-  child_data = g_list_sort (child_data, _cmp_by_track_then_priority);
+  child_data = g_list_sort (child_data,
+      _cmp_duration_limit_data_by_track_then_priority);
 
   start = child_data;
 
@@ -2680,6 +2684,17 @@ ges_clip_get_duration_limit (GESClip * clip)
   return clip->priv->duration_limit;
 }
 
+static gint
+_cmp_children_by_priority (gconstpointer a_p, gconstpointer b_p)
+{
+  const GESTimelineElement *a = a_p, *b = b_p;
+  if (a->priority > b->priority)
+    return 1;
+  else if (a->priority < b->priority)
+    return -1;
+  return 0;
+}
+
 /**
  * ges_clip_get_top_effects:
  * @clip: A #GESClip
@@ -2702,16 +2717,13 @@ ges_clip_get_top_effects (GESClip * clip)
   GST_DEBUG_OBJECT (clip, "Getting the %i top effects", clip->priv->nb_effects);
   ret = NULL;
 
-  /* should be sorted by priority, but make sure */
-  _ges_container_sort_children (GES_CONTAINER (clip));
-
   for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next) {
     child = tmp->data;
     if (_IS_TOP_EFFECT (child))
       ret = g_list_append (ret, gst_object_ref (child));
   }
 
-  return ret;
+  return g_list_sort (ret, _cmp_children_by_priority);
 }
 
 static gboolean
@@ -3227,42 +3239,673 @@ ges_clip_find_track_elements (GESClip * clip, GESTrack * track,
   return ret;
 }
 
+/* Convert from an internal time of a child within a clip to a
+ * ===========================================================
+ * timeline time
+ * =============
+ *
+ * Given an internal time T for some child in a clip, we want to know
+ * what the corresponding time in the timeline is.
+ *
+ * If the time T is between the in-point and out-point of the child,
+ * then we can convert to the timeline coordinates by answering:
+ *
+ * a) "What is the timeline time at which the internal data from the child
+ * found at time T appears in the timeline output?"
+ *
+ * If the time T is after the out-point of the child, we instead want to
+ * answer:
+ *
+ * b) "If we extended the clip indefinetly in the timeline, what would be
+ * the timeline time at which the internal data from the child found at
+ * time T would appear in the timeline output?"
+ *
+ * However, if the time T is before the in-point of the child, we instead
+ * want to answer a more subtle question:
+ *
+ * c) "If we set the 'in-point' of the child to T, what would we need to
+ * set the 'start' of the clip to such that the internal data from the
+ * child currently found at the *beginning* of the clip would then appear
+ * at the same timeline time?"
+ *
+ * E.g. consider the following children of a clip, all in the same track,
+ * and all active:
+ *                                T
+ *                                :
+ *          +=====================:======+
+ *          |                   _/ \_    |
+ *          |         source   ~(o_o)~   |
+ *          |                   / @ \    |
+ *          +=====================:======+
+ *          i                     :
+ *                                :
+ *          +=====================:======+
+ *          |       time-effect0  :      |  | g0
+ *          +=====================:======+  v
+ *                                :
+ *          +=====================:======+
+ *          |         overlay     :      |
+ *          +=====================:======+
+ *          i'                    :
+ *                                :
+ *          +=====================:======+
+ *          |       time-effect1  :      |  | g1
+ *          +=====================:======+  v
+ *                                :
+ * -------------------------------:-------------------timeline
+ *          S                     X
+ *
+ * where i is the in-point of the source and i' is the in-point of the
+ * overlay. Also, g0 is the sink_to_source translation function for the
+ * first time effect, and g1 is the same for the second. S is the start of
+ * the clip. The ~(o_o)~ figure is the data that appears in the source at
+ * T.
+ *
+ * Essentially, question a) wants us to convert from the time T, where the
+ * data is, which is in the internal time coordinates of the source, to
+ * the timeline time X. First, we subtract i to convert from the internal
+ * source coordinates of the source to the external source coordinates of
+ * the source, then we apply the sink_to_source translation functions,
+ * which act on external source coordinates, then add 'start' to finally
+ * convert to the timeline coordinates. So overall we have
+ *
+ *   X = S + g1(g0(T - i))
+ *
+ * To answer b), T would be beyond the end of the clip. Since g1 and g0
+ * can convert beyond the end time, we similarly compute
+ *
+ *   X = S + g1(g0(T - i))
+ *
+ * The user themselves should note that this could exceed the max-duration
+ * of any of the children.
+ *
+ * Now consider
+ *
+ *    T
+ *    :
+ *    :     +============================+
+ *    :      \_                          |
+ *    :     _o)~        source           |
+ *    :     @ \                          |
+ *    :     +============================+
+ *    :     i
+ *    :
+ *    :     +============================+
+ *    :     |       time-effect0         |  | g0
+ *    :     +============================+  v
+ *    :
+ *    :     +============================+
+ *    :     |           overlay          |
+ *    :     +============================+
+ *    :     i'
+ *    :
+ *    :     +============================+
+ *    :     |       time-effect1         |  | g1
+ *    :     +============================+  v
+ *    :
+ * ---:-----------------------------------------------timeline
+ *    X     S
+ *
+ * To do the same as a), we would need to be able to convert from T to X,
+ * but this isn't defined since the children do not extend to here. More
+ * specifically, the functions g0 and g1 are not defined for negative
+ * times. Instead, we want to answer question c). That is, we want to know
+ * what we should set the start of the clip to to keep the figure at the
+ * same timeline position if we change the in-point of the source to T.
+ *
+ * First, if we set the in-point to T, then we would have
+ *
+ *          T
+ *          :
+ *          +============================+
+ *          |   _/ \_                    |
+ *          |  ~(o_o)~        source     |
+ *          |   / @ \                    |
+ *          +============================+
+ *          :     i
+ *          :     :
+ *          +=====:======================+
+ *          |     :       time-effect0   |  | g0
+ *          +=====:======================+  v
+ *          :     :
+ *          +=====:======================+
+ *          |     :           overlay    |
+ *          +=====:======================+
+ *          :     :
+ *          +=====:======================+
+ *          |     :       time-effect1   |  | g1
+ *          +=====:======================+  v
+ *          :     :
+ * ---:-----:-----:-----------------------------------timeline
+ *    X     S     Y
+ *
+ * In order to make the figure appear at 'start' again, we would need to
+ * reduce the start of the clip by the difference between S and Y, where
+ * Y is the conversion of the previous in-point i to the timeline time.
+ *
+ * Thus,
+ *
+ *   X = S - (Y - S)
+ *     = S - (S + g1(g0(i - T)) - S)
+ *     = S - g1(g0(i - T))
+ *
+ * If this would be negative, the conversion will not be possible.
+ *
+ * Note, we are relying on the *assumption* that the translation functions
+ * *do not* change when we change the in-point. GESBaseEffect only claims
+ * to support such time effects.
+ *
+ * Note that if g0 and g1 are simply identities, and we translate the
+ * internal time using a) and b), we calculate
+ *
+ *   S + (T - i)
+ *
+ * and for c), we calculate
+ *
+ *   S - (i - T) = S + (T - i)
+ *
+ * In summary, if we are converting from internal time T to a timeline
+ * time the return is
+ *
+ *   G(T) = {  S + g1(g0(T - i))   if T >= i,
+ *          {  S - g1(g0(i - T))   otherwise.
+ *
+ * Note that the overlay did not play a role since it overall translates
+ * all received times by the identity. Note that we could similarly want
+ * to convert from an internal time in the overlay to the timeline time.
+ * This would be given by
+ *
+ *   S + g1(T - i')   if T >= i',
+ *   S - g1(i' - T)   otherwise.
+ *
+ *
+ * Convert from a timeline time to an internal time of a child
+ * ===========================================================
+ * in a clip
+ * =========
+ *
+ * We basically want to reverse the previous conversion. Specifically,
+ * when the timeline time X is between the start and end of the clip we
+ * want to answer:
+ *
+ * d) "What is the internal time at which the data from the child that
+ * appears in the timeline at time X is created in the child?"
+ *
+ * If the time X is after the end of the clip, we instead want to answer:
+ *
+ * e) "If we extended the clip indefinetly in the timeline, what would be
+ * the internal time at which the data from the child that appears in the
+ * timeline at time T would be created in the child?"
+ *
+ * However, if the time X is before the start of the child, we instead
+ * want to answer:
+ *
+ * f) "If we set the 'start' of the clip to X, what would we need to
+ * set the 'in-point' of the clip to such that the internal data from the
+ * child currently found at the *beginning* of the clip would then appear
+ * at the same timeline time?"
+ *
+ * Following the same arguments, these would all be answered by
+ *
+ *   F(X) = {  i + f0(f1(X - S))   if X >= S,
+ *          {  i - f0(f1(S - X))   otherwise.
+ *
+ * where f0 and f1 are the corresponding source_to_sink translation
+ * functions, which should be close reverses of g0 and g1, respectively.
+ *
+ * Note that this does indeed reverse the internal to timeline conversion:
+ *
+ *   F(G(T)) = {  i + f0(f1(G(T) - S))   if G(T) >= S,
+ *             {  i - f0(f1(S - G(T)))   otherwise.
+ *
+ * but, since g1 and g0 map from [0,inf) to [0,inf),
+ *
+ *   G(T) - S = {  + g1(g0(T - i))   if T >= i,
+ *              {  - g1(g0(i - T))   otherwise.
+ *            { >= 0                 if T >= i,
+ *            { = 0                  if (T < i and g1(g0(i - T)) = 0)
+ *            { < 0                  otherwise.
+ *
+ * =>   ( G(T) >= S  <==>  T >= i or (T < i and g1(g0(i - T)) = 0) )
+ *
+ * therefore
+ *   F(G(T)) = {  i + f0(f1(g1(g0(T - i))))   if T >= i,
+ *             {  i + f0(f1(0))               if T < i
+ *             {                              and g1(g0(i - T)) = 0,
+ *             {  i - f0(f1(g1(g0(i - T))))   otherwise
+ *
+ *           = {  i + f0(f1(g1(g0(T - i))))   if T >= i,
+ *             {  i - f0(f1(g1(g0(i - T))))   otherwise
+ *
+ *           = T
+ *
+ * because f1 reverses g1, and f0 reverses g0.
+ */
+
+/* returns higher priority first */
+static GList *
+_active_time_effects_in_track_after_priority (GESClip * clip,
+    GESTrack * track, guint32 priority)
+{
+  GList *tmp, *list = NULL;
+
+  for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next) {
+    GESTrackElement *child = tmp->data;
+
+    if (GES_IS_TIME_EFFECT (child)
+        && ges_track_element_get_track (child) == track
+        && ges_track_element_is_active (child)
+        && _PRIORITY (child) < priority)
+      list = g_list_prepend (list, child);
+  }
+
+  return g_list_sort (list, _cmp_children_by_priority);
+}
+
 /**
- * ges_clip_get_timeline_time_from_source_frame:
+ * ges_clip_get_timeline_time_from_internal_time:
+ * @clip: A #GESClip
+ * @child: An #GESTrackElement:active child of @clip with a
+ * #GESTrackElement:track
+ * @internal_time: A time in the internal time coordinates of @child
+ * @error: (nullable): Return location for an error
+ *
+ * Convert the internal source time from the child to a timeline time.
+ * This will take any time effects placed on the clip into account (see
+ * #GESBaseEffect for what time effects are supported, and how to
+ * declare them in GES).
+ *
+ * When @internal_time is above the #GESTimelineElement:in-point of
+ * @child, this will return the timeline time at which the internal
+ * content found at @internal_time appears in the output of the timeline's
+ * track. For example, this would let you know where in the timeline a
+ * particular scene in a media file would appear.
+ *
+ * This will be done assuming the clip has an indefinite end, so the
+ * timeline time may be beyond the end of the clip, or even breaking its
+ * #GESClip:duration-limit.
+ *
+ * If, instead, @internal_time is below the current
+ * #GESTimelineElement:in-point of @child, this will return what you would
+ * need to set the #GESTimelineElement:start of @clip to if you set the
+ * #GESTimelineElement:in-point of @child to @internal_time and wanted to
+ * keep the content of @child currently found at the current
+ * #GESTimelineElement:start of @clip at the same timeline position. If
+ * this would be negative, the conversion fails. This is useful for
+ * determining what position to use in a #GES_EDIT_MODE_TRIM if you wish
+ * to trim to a specific point in the internal content, such as a
+ * particular scene in a media file.
+ *
+ * Note that whilst a clip has no time effects, this second return is
+ * equivalent to finding the timeline time at which the content of @child
+ * at @internal_time would be found in the timeline if it had indefinite
+ * extent in both directions. However, with non-linear time effects this
+ * second return will be more distinct.
+ *
+ * In either case, the returned time would be appropriate to use in
+ * ges_timeline_element_edit() for #GES_EDIT_MODE_TRIM, and similar, if
+ * you wish to use a particular internal point as a reference.
+ *
+ * See ges_clip_get_internal_time_from_timeline_time(), which performs the
+ * reverse, or ges_clip_get_timeline_time_from_source_frame() which does
+ * the same conversion, but using frame numbers.
+ *
+ * Returns: The time in the timeline coordinates corresponding to
+ * @internal_time, or #GST_CLOCK_TIME_NONE if the conversion could not be
+ * performed.
+ */
+GstClockTime
+ges_clip_get_timeline_time_from_internal_time (GESClip * clip,
+    GESTrackElement * child, GstClockTime internal_time, GError ** error)
+{
+  GstClockTime inpoint, start, external_time;
+  gboolean decrease;
+  GESTrack *track;
+  GList *tmp, *time_effects;
+
+  g_return_val_if_fail (GES_IS_CLIP (clip), GST_CLOCK_TIME_NONE);
+  g_return_val_if_fail (GES_IS_TRACK_ELEMENT (child), GST_CLOCK_TIME_NONE);
+  g_return_val_if_fail (!error || !*error, GST_CLOCK_TIME_NONE);
+
+  if (!g_list_find (GES_CONTAINER_CHILDREN (clip), child)) {
+    GST_WARNING_OBJECT (clip, "The track element %" GES_FORMAT " is not "
+        "a child of the clip", GES_ARGS (child));
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  track = ges_track_element_get_track (child);
+
+  if (!track) {
+    GST_WARNING_OBJECT (clip, "Cannot convert the internal time of the "
+        "child %" GES_FORMAT " to a timeline time because it is not part "
+        "of a track", GES_ARGS (child));
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  if (!ges_track_element_is_active (child)) {
+    GST_WARNING_OBJECT (clip, "Cannot convert the internal time of the "
+        "child %" GES_FORMAT " to a timeline time because it is not "
+        "active in its track", GES_ARGS (child));
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  if (internal_time == GST_CLOCK_TIME_NONE)
+    return GST_CLOCK_TIME_NONE;
+
+  inpoint = _INPOINT (child);
+  if (inpoint <= internal_time) {
+    decrease = FALSE;
+    external_time = internal_time - inpoint;
+  } else {
+    decrease = TRUE;
+    external_time = inpoint - internal_time;
+  }
+
+  time_effects = _active_time_effects_in_track_after_priority (clip, track,
+      _PRIORITY (child));
+
+  /* currently ordered with highest priority (closest to the timeline)
+   * first, with @child being at the *end* of the list.
+   * Want to reverse this so we can convert from the child towards the
+   * timeline */
+  time_effects = g_list_reverse (time_effects);
+
+  for (tmp = time_effects; tmp; tmp = tmp->next) {
+    GESBaseEffect *effect = tmp->data;
+    GHashTable *values = ges_base_effect_get_time_property_values (effect);
+
+    external_time = ges_base_effect_translate_sink_to_source_time (effect,
+        external_time, values);
+    g_hash_table_unref (values);
+  }
+
+  g_list_free (time_effects);
+
+  if (!GST_CLOCK_TIME_IS_VALID (external_time))
+    return GST_CLOCK_TIME_NONE;
+
+  start = _START (clip);
+
+  if (!decrease)
+    return start + external_time;
+
+  if (external_time > start) {
+    GST_INFO_OBJECT (clip, "Cannot convert the internal time %"
+        GST_TIME_FORMAT " of the child %" GES_FORMAT " to a timeline "
+        "time because it would lie before the start of the timeline",
+        GST_TIME_ARGS (internal_time), GES_ARGS (child));
+
+    g_set_error (error, GES_ERROR, GES_ERROR_NEGATIVE_TIME,
+        "The internal time %" GST_TIME_FORMAT " of child \"%s\" "
+        "would correspond to a negative start of -%" GST_TIME_FORMAT
+        " for the clip \"%s\"", GST_TIME_ARGS (internal_time),
+        GES_TIMELINE_ELEMENT_NAME (child),
+        GST_TIME_ARGS (external_time - start),
+        GES_TIMELINE_ELEMENT_NAME (clip));
+
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  return start - external_time;
+}
+
+/**
+ * ges_clip_get_internal_time_from_timeline_time:
  * @clip: A #GESClip
- * @frame_number: The frame number to get the corresponding timestamp in the
- *                timeline coordinates
- * @err: A #GError set on errors
+ * @child: An #GESTrackElement:active child of @clip with a
+ * #GESTrackElement:track
+ * @timeline_time: A time in the timeline time coordinates
+ * @error: (nullable): Return location for an error
  *
- * This method allows you to convert a frame number into a #GstClockTime, this
- * can be used to either seek to a particular frame in the timeline or to later
- * on edit @self with that timestamp.
+ * Convert the timeline time to an internal source time of the child.
+ * This will take any time effects placed on the clip into account (see
+ * #GESBaseEffect for what time effects are supported, and how to
+ * declare them in GES).
+ *
+ * When @timeline_time is above the #GESTimelineElement:start of @clip,
+ * this will return the internal time at which the content that appears at
+ * @timeline_time in the output of the timeline is created in @child. For
+ * example, if @timeline_time corresponds to the current seek position,
+ * this would let you know which part of a media file is being read.
+ *
+ * This will be done assuming the clip has an indefinite end, so the
+ * internal time may be beyond the current out-point of the child, or even
+ * its #GESTimelineElement:max-duration.
+ *
+ * If, instead, @timeline_time is below the current
+ * #GESTimelineElement:start of @clip, this will return what you would
+ * need to set the #GESTimelineElement:in-point of @child to if you set
+ * the #GESTimelineElement:start of @clip to @timeline_time and wanted
+ * to keep the content of @child currently found at the current
+ * #GESTimelineElement:start of @clip at the same timeline position. If
+ * this would be negative, the conversion fails. This is useful for
+ * determining what #GESTimelineElement:in-point would result from a
+ * #GES_EDIT_MODE_TRIM to @timeline_time.
+ *
+ * Note that whilst a clip has no time effects, this second return is
+ * equivalent to finding the internal time at which the content that
+ * appears at @timeline_time in the timeline can be found in @child if it
+ * had indefinite extent in both directions. However, with non-linear time
+ * effects this second return will be more distinct.
+ *
+ * In either case, the returned time would be appropriate to use for the
+ * #GESTimelineElement:in-point or #GESTimelineElement:max-duration of the
+ * child.
+ *
+ * See ges_clip_get_timeline_time_from_internal_time(), which performs the
+ * reverse.
+ *
+ * Returns: The time in the internal coordinates of @child corresponding
+ * to @timeline_time, or #GST_CLOCK_TIME_NONE if the conversion could not
+ * be performed.
+ */
+GstClockTime
+ges_clip_get_internal_time_from_timeline_time (GESClip * clip,
+    GESTrackElement * child, GstClockTime timeline_time, GError ** error)
+{
+  GstClockTime inpoint, start, external_time;
+  gboolean decrease;
+  GESTrack *track;
+  GList *tmp, *time_effects;
+
+  g_return_val_if_fail (GES_IS_CLIP (clip), GST_CLOCK_TIME_NONE);
+  g_return_val_if_fail (GES_IS_TRACK_ELEMENT (child), GST_CLOCK_TIME_NONE);
+  g_return_val_if_fail (!error || !*error, GST_CLOCK_TIME_NONE);
+
+  if (!g_list_find (GES_CONTAINER_CHILDREN (clip), child)) {
+    GST_WARNING_OBJECT (clip, "The track element %" GES_FORMAT " is not "
+        "a child of the clip", GES_ARGS (child));
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  track = ges_track_element_get_track (child);
+
+  if (!track) {
+    GST_WARNING_OBJECT (clip, "Cannot convert the timeline time to an "
+        "internal time of child %" GES_FORMAT " because it is not part "
+        "of a track", GES_ARGS (child));
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  if (!ges_track_element_is_active (child)) {
+    GST_WARNING_OBJECT (clip, "Cannot convert the timeline time to an "
+        "internal time of child %" GES_FORMAT " because it is not active "
+        "in its track", GES_ARGS (child));
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  if (timeline_time == GST_CLOCK_TIME_NONE)
+    return GST_CLOCK_TIME_NONE;
+
+  start = _START (clip);
+  if (start <= timeline_time) {
+    decrease = FALSE;
+    external_time = timeline_time - start;
+  } else {
+    decrease = TRUE;
+    external_time = start - timeline_time;
+  }
+
+  time_effects = _active_time_effects_in_track_after_priority (clip, track,
+      _PRIORITY (child));
+
+  /* currently ordered with highest priority (closest to the timeline)
+   * first, with @child being at the *end* of the list, which is what we
+   * want */
+
+  for (tmp = time_effects; tmp; tmp = tmp->next) {
+    GESBaseEffect *effect = tmp->data;
+    GHashTable *values = ges_base_effect_get_time_property_values (effect);
+
+    external_time = ges_base_effect_translate_source_to_sink_time (effect,
+        external_time, values);
+    g_hash_table_unref (values);
+  }
+
+  g_list_free (time_effects);
+
+  if (!GST_CLOCK_TIME_IS_VALID (external_time))
+    return GST_CLOCK_TIME_NONE;
+
+  inpoint = _INPOINT (child);
+
+  if (!decrease)
+    return inpoint + external_time;
+
+  if (external_time > inpoint) {
+    GST_INFO_OBJECT (clip, "Cannot convert the timeline time %"
+        GST_TIME_FORMAT " to an internal time of the child %"
+        GES_FORMAT " because it would be before the element has any "
+        "internal content", GST_TIME_ARGS (timeline_time), GES_ARGS (child));
+
+    g_set_error (error, GES_ERROR, GES_ERROR_NEGATIVE_TIME,
+        "The timeline time %" GST_TIME_FORMAT " would correspond to "
+        "a negative in-point of -%" GST_TIME_FORMAT " for the child "
+        "\"%s\" under clip \"%s\"", GST_TIME_ARGS (timeline_time),
+        GST_TIME_ARGS (external_time - inpoint),
+        GES_TIMELINE_ELEMENT_NAME (child), GES_TIMELINE_ELEMENT_NAME (clip));
+
+    return GST_CLOCK_TIME_NONE;
+  }
+
+  return inpoint - external_time;
+}
+
+static GstClockTime
+_convert_core_time (GESClip * clip, GstClockTime time, gboolean to_timeline,
+    gboolean * no_core, GError ** error)
+{
+  GList *tmp;
+  GstClockTime converted = GST_CLOCK_TIME_NONE;
+  GstClockTime half_frame;
+  GESTimeline *timeline = GES_TIMELINE_ELEMENT_TIMELINE (clip);
+  GESClipAsset *asset =
+      GES_CLIP_ASSET (ges_extractable_get_asset (GES_EXTRACTABLE (clip)));
+
+  if (no_core)
+    *no_core = TRUE;
+
+  if (to_timeline)
+    half_frame = timeline ? ges_timeline_get_frame_time (timeline, 1) : 0;
+  else
+    half_frame = ges_clip_asset_get_frame_time (asset, 1);
+  half_frame = GST_CLOCK_TIME_IS_VALID (half_frame) ? half_frame / 2 : 0;
+
+  for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next) {
+    GESTrackElement *child = tmp->data;
+    GESTrack *track = ges_track_element_get_track (child);
+
+    if (_IS_CORE_CHILD (child) && track && ges_track_element_is_active (child)
+        && ges_track_element_has_internal_source (child)) {
+      GstClockTime tmp_time;
+      GError *convert_error = NULL;
+
+      if (no_core)
+        *no_core = FALSE;
+
+      if (to_timeline)
+        tmp_time =
+            ges_clip_get_timeline_time_from_internal_time (clip, child, time,
+            &convert_error);
+      else
+        tmp_time =
+            ges_clip_get_internal_time_from_timeline_time (clip, child, time,
+            &convert_error);
+
+      if (!GST_CLOCK_TIME_IS_VALID (converted)) {
+        converted = tmp_time;
+      } else if (!GST_CLOCK_TIME_IS_VALID (tmp_time)) {
+        GST_WARNING_OBJECT (clip, "The calculated %s time for %s time %"
+            GST_TIME_FORMAT " using core child %" GES_FORMAT " is not "
+            "defined, but it had a definite value of %" GST_TIME_FORMAT
+            " for another core child", to_timeline ? "timeline" : "internal",
+            to_timeline ? "internal" : "timeline", GST_TIME_ARGS (time),
+            GES_ARGS (child), GST_TIME_ARGS (converted));
+      } else if (tmp_time != converted) {
+        GstClockTime diff = (tmp_time > converted) ?
+            tmp_time - converted : converted - tmp_time;
+
+        if (diff > half_frame) {
+          GST_WARNING_OBJECT (clip, "The calculated %s time for %s time %"
+              GST_TIME_FORMAT " using core child %" GES_FORMAT " is %"
+              GST_TIME_FORMAT ", which is different from the value of %"
+              GST_TIME_FORMAT " calculated using a different core child",
+              to_timeline ? "timeline" : "internal",
+              to_timeline ? "internal" : "timeline", GST_TIME_ARGS (time),
+              GES_ARGS (child), GST_TIME_ARGS (tmp_time),
+              GST_TIME_ARGS (converted));
+        }
+
+        /* prefer result from video tracks */
+        if (GES_IS_VIDEO_TRACK (track))
+          converted = tmp_time;
+      }
+      if (convert_error) {
+        if (error) {
+          g_clear_error (error);
+          *error = convert_error;
+        } else {
+          g_error_free (convert_error);
+        }
+      }
+    }
+  }
+
+  return converted;
+}
+
+/**
+ * ges_clip_get_timeline_time_from_source_frame:
+ * @clip: A #GESClip
+ * @frame_number: The frame number to get the corresponding timestamp of
+ * in the timeline coordinates
+ * @error: (nullable): Return location for an error
  *
- * This method should be use specifically in the case where you want to trim the
- * clip to a particular frame.
+ * Convert the source frame number to a timeline time. This acts the same
+ * as ges_clip_get_timeline_time_from_internal_time() using the core
+ * children of the clip and using the frame number to specify the internal
+ * position, rather than a timestamp.
  *
- * The returned timestamp is in the global #GESTimeline time coordinates of @self, not
- * in the internal time coordinates. In practice, this means that you can not use
- * that time to set the clip #GESTimelineElement:in-point but it can be used in
- * the timeline editing API, for example as the @position argument of the
- * #ges_timeline_element_edit method.
+ * The returned timeline time can be used to seek or edit to a specific
+ * frame.
  *
- * Note that you can get the frame timestamp of a particular clip asset with
- * #ges_clip_asset_get_frame_time.
+ * Note that you can get the frame timestamp of a particular clip asset
+ * with ges_clip_asset_get_frame_time().
  *
- * Returns: The timestamp corresponding to @frame_number in the element source
- * in the timeline coordinates.
+ * Returns: The timestamp corresponding to @frame_number in the core
+ * children of @clip, in the timeline coordinates, or #GST_CLOCK_TIME_NONE
+ * if the conversion could not be performed.
  */
 GstClockTime
 ges_clip_get_timeline_time_from_source_frame (GESClip * clip,
-    GESFrameNumber frame_number, GError ** err)
+    GESFrameNumber frame_number, GError ** error)
 {
+  GstClockTime timeline_time = GST_CLOCK_TIME_NONE;
   GstClockTime frame_ts;
   GESClipAsset *asset;
-  GstClockTimeDiff inpoint_diff;
 
   g_return_val_if_fail (GES_IS_CLIP (clip), GST_CLOCK_TIME_NONE);
-  g_return_val_if_fail (!err || !*err, GST_CLOCK_TIME_NONE);
+  g_return_val_if_fail (!error || !*error, GST_CLOCK_TIME_NONE);
 
   if (!GES_FRAME_NUMBER_IS_VALID (frame_number))
     return GST_CLOCK_TIME_NONE;
@@ -3272,15 +3915,16 @@ ges_clip_get_timeline_time_from_source_frame (GESClip * clip,
   if (!GST_CLOCK_TIME_IS_VALID (frame_ts))
     return GST_CLOCK_TIME_NONE;
 
-  inpoint_diff = GST_CLOCK_DIFF (frame_ts, GES_TIMELINE_ELEMENT_INPOINT (clip));
-  if (GST_CLOCK_DIFF (inpoint_diff, _START (clip)) < 0) {
-    g_set_error (err, GES_ERROR, GES_ERROR_INVALID_FRAME_NUMBER,
-        "Requested frame %" G_GINT64_FORMAT
-        " would be outside the timeline.", frame_number);
-    return GST_CLOCK_TIME_NONE;
+  timeline_time = _convert_core_time (clip, frame_ts, TRUE, NULL, error);
+
+  if (error && *error) {
+    g_clear_error (error);
+    g_set_error (error, GES_ERROR, GES_ERROR_INVALID_FRAME_NUMBER,
+        "Requested frame %" G_GINT64_FORMAT " would be outside the "
+        "timeline.", frame_number);
   }
 
-  return GST_CLOCK_DIFF (inpoint_diff, _START (clip));
+  return timeline_time;
 }
 
 /**
index bf62eac7700c3f0d43edeff5c2e132bed0910a31..bf28a9b8ff5baa37cce7d6ff21213f5652cebc30 100644 (file)
@@ -216,9 +216,19 @@ GESClip*     ges_clip_split_full                           (GESClip *clip,
                                                             GError ** error);
 
 GES_API
+GstClockTime ges_clip_get_internal_time_from_timeline_time (GESClip * clip,
+                                                            GESTrackElement * child,
+                                                            GstClockTime timeline_time,
+                                                            GError ** error);
+GES_API
+GstClockTime ges_clip_get_timeline_time_from_internal_time (GESClip * clip,
+                                                            GESTrackElement * child,
+                                                            GstClockTime internal_time,
+                                                            GError ** error);
+GES_API
 GstClockTime ges_clip_get_timeline_time_from_source_frame (GESClip * clip,
                                                            GESFrameNumber frame_number,
-                                                           GError ** err);
+                                                           GError ** error);
 
 GES_API
 GstClockTime ges_clip_get_duration_limit (GESClip * clip);
index 13053552c691df6503ddc3cdfcdf953232448d48..aa9e3856fafb160251da60449d91b18c7cc74575 100644 (file)
  * #GESTimelineElement:start, or having insufficient internal
  * content to last for the desired #GESTimelineElement:duration).
  *
+ * ## Time Coordinates
+ *
+ * There are three main sets of time coordinates to consider when using
+ * timeline elements:
+ *
+ * + Timeline coordinates: these are the time coordinates used in the
+ *   output of the timeline in its #GESTrack-s. Each track share the same
+ *   coordinates, so there is only one set of coordinates for the
+ *   timeline. These extend indefinitely from 0. The times used for
+ *   editing (including setting #GESTimelineElement:start and
+ *   #GESTimelineElement:duration) use these coordinates, since these
+ *   define when an element is present and for how long the element lasts
+ *   for in the timeline.
+ * + Internal source coordinates: these are the time coordinates used
+ *   internally at the element's output. This is only really defined for
+ *   #GESTrackElement-s, where it refers to time coordinates used at the
+ *   final source pad of the wrapped #GstElement-s. However, these
+ *   coordinates may also be used in a #GESClip in reference to its
+ *   children. In particular, these are the coordinates used for
+ *   #GESTimelineElement:in-point and #GESTimelineElement:max-duration.
+ * + Internal sink coordinates: these are the time coordinates used
+ *   internally at the element's input. A #GESSource has no input, so
+ *   these would be undefined. Otherwise, for most #GESTrackElement-s
+ *   these will be the same set of coordinates as the internal source
+ *   coordinates because the element does not change the timing
+ *   internally. Only #GESBaseEffect can support elements where these
+ *   are different. See #GESBaseEffect for more information.
+ *
+ * You can determine the timeline time for a given internal source time
+ * in a #GESTrack in a #GESClip using
+ * ges_clip_get_timeline_time_from_internal_time(), and vice versa using
+ * ges_clip_get_internal_time_from_timeline_time(), for the purposes of
+ * editing and setting timings properties.
+ *
  * ## Children Properties
  *
  * If a timeline element owns another #GstObject and wishes to expose
@@ -2366,7 +2400,7 @@ ges_timeline_element_get_layer_priority (GESTimelineElement * self)
  * @mode: The edit mode
  * @edge: The edge of @self where the edit should occur
  * @position: The edit position: a new location for the edge of @self
- * (in nanoseconds)
+ * (in nanoseconds) in the timeline coordinates
  * @error: (nullable): Return location for an error
  *
  * Edits the element within its timeline by adjusting its
index f12906f76d18caa8edcb0fde789394b0623388b8..9c9430cf1226af807fecea2b7836596d794259f1 100644 (file)
@@ -4776,7 +4776,424 @@ GST_START_TEST (test_unchanged_after_layer_add_failure)
   gst_object_unref (clip1);
   gst_object_unref (timeline);
 
-  gst_deinit ();
+  ges_deinit ();
+}
+
+GST_END_TEST;
+
+#define _assert_timeline_to_internal(clip, child, in, expect_out) \
+{\
+  GError *error = NULL; \
+  GstClockTime found = ges_clip_get_internal_time_from_timeline_time ( \
+        clip, child, (in) * GST_SECOND, &error); \
+  GstClockTime expect =  expect_out * GST_SECOND; \
+  fail_unless (found == expect, "Conversion from timeline time %" \
+      GST_TIME_FORMAT " to the internal time of %" GES_FORMAT " gave %" \
+      GST_TIME_FORMAT " rather than the expected %" GST_TIME_FORMAT \
+      " (error: %s)", GST_TIME_ARGS ((in) * GST_SECOND), \
+      GES_ARGS (child), GST_TIME_ARGS (found), GST_TIME_ARGS (expect), \
+      error ? error->message : "None"); \
+  fail_if (error); \
+}
+
+#define _assert_timeline_to_internal_fails(clip, child, in, error_code) \
+{ \
+  GError *error = NULL; \
+  GstClockTime found = ges_clip_get_internal_time_from_timeline_time ( \
+        clip, child, (in) * GST_SECOND, &error); \
+  fail_if (GST_CLOCK_TIME_IS_VALID (found), "Conversion from timeline " \
+      "time %" GST_TIME_FORMAT " to the internal time of %" GES_FORMAT \
+      " successfully converted to %" GST_TIME_FORMAT " rather than " \
+      "GST_CLOCK_TIME_NONE", GST_TIME_ARGS ((in) * GST_SECOND), \
+      GES_ARGS (child), GST_TIME_ARGS (found)); \
+  assert_GESError (error, error_code); \
+}
+
+#define _assert_internal_to_timeline(clip, child, in, expect_out) \
+{\
+  GError *error = NULL; \
+  GstClockTime found = ges_clip_get_timeline_time_from_internal_time ( \
+        clip, child, (in) * GST_SECOND, &error); \
+  GstClockTime expect = expect_out * GST_SECOND; \
+  fail_unless (found == expect, "Conversion from the internal time %" \
+      GST_TIME_FORMAT " of %" GES_FORMAT " to the timeline time gave %" \
+      GST_TIME_FORMAT " rather than the expected %" GST_TIME_FORMAT, \
+      GST_TIME_ARGS ((in) * GST_SECOND), GES_ARGS (child), \
+      GST_TIME_ARGS (found), GST_TIME_ARGS (expect)); \
+  fail_if (error); \
+}
+
+#define _assert_internal_to_timeline_fails(clip, child, in, error_code) \
+{\
+  GError *error = NULL; \
+  GstClockTime found = ges_clip_get_timeline_time_from_internal_time ( \
+        clip, child, (in) * GST_SECOND, &error); \
+  fail_if (GST_CLOCK_TIME_IS_VALID (found), "Conversion from the " \
+      "internal time %" GST_TIME_FORMAT " of %" GES_FORMAT " to the " \
+      "timeline time gave %" GST_TIME_FORMAT " rather than " \
+      "GST_CLOCK_TIME_NONE", GST_TIME_ARGS ((in) * GST_SECOND), \
+      GES_ARGS (child), GST_TIME_ARGS (found)); \
+  assert_GESError (error, error_code); \
+}
+
+#define _assert_frame_to_timeline(clip, frame, expect_out) \
+{\
+  GError *error = NULL; \
+  GstClockTime found = ges_clip_get_timeline_time_from_source_frame ( \
+        clip, frame, &error); \
+  GstClockTime expect = expect_out * GST_SECOND; \
+  fail_unless (found == expect, "Conversion from the source frame %" \
+      G_GINT64_FORMAT " to the timeline time gave %" GST_TIME_FORMAT \
+      " rather than the expected %" GST_TIME_FORMAT, frame, \
+      GST_TIME_ARGS (found), GST_TIME_ARGS (expect)); \
+  fail_if (error); \
+}
+
+#define _assert_frame_to_timeline_fails(clip, frame, error_code) \
+{\
+  GError *error = NULL; \
+  GstClockTime found = ges_clip_get_timeline_time_from_source_frame ( \
+        clip, frame, &error); \
+  fail_if (GST_CLOCK_TIME_IS_VALID (found), "Conversion from the " \
+      "source frame %" G_GINT64_FORMAT " to the timeline time gave %" \
+      GST_TIME_FORMAT " rather than the expected GST_CLOCK_TIME_NONE", \
+      frame, GST_TIME_ARGS (found)); \
+  assert_GESError (error, error_code); \
+}
+
+GST_START_TEST (test_convert_time)
+{
+  GESTimeline *timeline;
+  GESTrack *track0, *track1;
+  GESAsset *asset;
+  GESLayer *layer;
+  GESClip *clip;
+  GESTrackElement *source0, *source1, *rate0, *rate1, *rate2, *overlay;
+  GValue val = G_VALUE_INIT;
+
+  ges_init ();
+
+  asset = ges_asset_request (GES_TYPE_TEST_CLIP,
+      "framerate=30/1, max-duration=93.0", NULL);
+  fail_unless (asset);
+
+  timeline = ges_timeline_new ();
+
+  track0 = GES_TRACK (ges_video_track_new ());
+  track1 = GES_TRACK (ges_video_track_new ());
+
+  fail_unless (ges_timeline_add_track (timeline, track0));
+  fail_unless (ges_timeline_add_track (timeline, track1));
+
+  layer = ges_timeline_append_layer (timeline);
+
+  clip = ges_layer_add_asset (layer, asset, 20 * GST_SECOND,
+      13 * GST_SECOND, 10 * GST_SECOND, GES_TRACK_TYPE_VIDEO);
+  fail_unless (clip);
+  CHECK_OBJECT_PROPS_MAX (clip, 20 * GST_SECOND, 13 * GST_SECOND,
+      10 * GST_SECOND, 93 * GST_SECOND);
+
+  source0 =
+      ges_clip_find_track_element (clip, track0, GES_TYPE_VIDEO_TEST_SOURCE);
+  source1 =
+      ges_clip_find_track_element (clip, track1, GES_TYPE_VIDEO_TEST_SOURCE);
+
+  rate0 = GES_TRACK_ELEMENT (ges_effect_new ("videorate"));
+  rate1 = GES_TRACK_ELEMENT (ges_effect_new ("videorate"));
+  rate2 = GES_TRACK_ELEMENT (ges_effect_new ("videorate"));
+  overlay = GES_TRACK_ELEMENT (ges_effect_new ("textoverlay"));
+  ges_track_element_set_has_internal_source (overlay, TRUE);
+  /* enough internal content to last 10 seconds at a rate of 4.0 */
+  assert_set_inpoint (overlay, 7 * GST_SECOND);
+  assert_set_max_duration (overlay, 50 * GST_SECOND);
+
+  fail_unless (ges_track_add_element (track0, rate0));
+  fail_unless (ges_track_add_element (track1, rate1));
+  fail_unless (ges_track_add_element (track1, rate2));
+  fail_unless (ges_track_add_element (track1, overlay));
+
+  _assert_add (clip, rate0);
+  _assert_add (clip, rate2);
+  _assert_add (clip, overlay);
+  _assert_add (clip, rate1);
+
+  /* in track0:
+   *
+   * source0 -> rate0 -> out
+   *
+   * in track1:
+   *
+   * source1 -> rate1 -> overlay -> rate2 -> out
+   */
+
+  g_value_init (&val, G_TYPE_DOUBLE);
+
+  _assert_rate_equal (rate0, "rate", 1.0, val);
+  _assert_rate_equal (rate1, "rate", 1.0, val);
+  _assert_rate_equal (rate2, "rate", 1.0, val);
+
+  /* without rates */
+
+  /* start of the clip */
+  _assert_internal_to_timeline (clip, source0, 13, 20);
+  _assert_internal_to_timeline (clip, source1, 13, 20);
+  _assert_internal_to_timeline (clip, overlay, 7, 20);
+  _assert_frame_to_timeline (clip, 390, 20);
+  _assert_timeline_to_internal (clip, source0, 20, 13);
+  _assert_timeline_to_internal (clip, source1, 20, 13);
+  _assert_timeline_to_internal (clip, overlay, 20, 7);
+
+  /* middle of the clip */
+  _assert_internal_to_timeline (clip, source0, 18, 25);
+  _assert_internal_to_timeline (clip, source1, 18, 25);
+  _assert_internal_to_timeline (clip, overlay, 12, 25);
+  _assert_frame_to_timeline (clip, 540, 25);
+  _assert_timeline_to_internal (clip, source0, 25, 18);
+  _assert_timeline_to_internal (clip, source1, 25, 18);
+  _assert_timeline_to_internal (clip, overlay, 25, 12);
+
+  /* end of the clip */
+  _assert_internal_to_timeline (clip, source0, 23, 30);
+  _assert_internal_to_timeline (clip, source1, 23, 30);
+  _assert_internal_to_timeline (clip, overlay, 17, 30);
+  _assert_frame_to_timeline (clip, 690, 30);
+  _assert_timeline_to_internal (clip, source0, 30, 23);
+  _assert_timeline_to_internal (clip, source1, 30, 23);
+  _assert_timeline_to_internal (clip, overlay, 30, 17);
+
+  /* beyond the end of the clip */
+  /* exceeds the max-duration of the elements, but that is ok */
+  _assert_internal_to_timeline (clip, source0, 123, 130);
+  _assert_internal_to_timeline (clip, source1, 123, 130);
+  _assert_internal_to_timeline (clip, overlay, 117, 130);
+  _assert_frame_to_timeline (clip, 3690, 130);
+  _assert_timeline_to_internal (clip, source0, 130, 123);
+  _assert_timeline_to_internal (clip, source1, 130, 123);
+  _assert_timeline_to_internal (clip, overlay, 130, 117);
+
+  /* before the start of the clip */
+  _assert_internal_to_timeline (clip, source0, 8, 15);
+  _assert_internal_to_timeline (clip, source1, 8, 15);
+  _assert_internal_to_timeline (clip, overlay, 2, 15);
+  _assert_frame_to_timeline (clip, 240, 15);
+  _assert_timeline_to_internal (clip, source0, 15, 8);
+  _assert_timeline_to_internal (clip, source1, 15, 8);
+  _assert_timeline_to_internal (clip, overlay, 15, 2);
+
+  /* too early for overlay */
+  _assert_timeline_to_internal (clip, source0, 10, 3);
+  _assert_timeline_to_internal (clip, source1, 10, 3);
+  _assert_timeline_to_internal_fails (clip, overlay, 10,
+      GES_ERROR_NEGATIVE_TIME);
+
+  /* too early for sources */
+  _assert_timeline_to_internal_fails (clip, source0, 5,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_timeline_to_internal_fails (clip, source1, 5,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_timeline_to_internal_fails (clip, overlay, 5,
+      GES_ERROR_NEGATIVE_TIME);
+
+  assert_set_start (clip, 10 * GST_SECOND);
+
+  /* too early in the timeline */
+  _assert_internal_to_timeline_fails (clip, source0, 2,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_internal_to_timeline_fails (clip, source1, 2,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_internal_to_timeline (clip, overlay, 2, 5);
+  _assert_frame_to_timeline_fails (clip, 60, GES_ERROR_INVALID_FRAME_NUMBER);
+
+  assert_set_start (clip, 6 * GST_SECOND);
+  _assert_internal_to_timeline_fails (clip, source0, 6,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_internal_to_timeline_fails (clip, source1, 6,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_internal_to_timeline_fails (clip, overlay, 0,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_frame_to_timeline_fails (clip, 180, GES_ERROR_INVALID_FRAME_NUMBER);
+
+  assert_set_start (clip, 20 * GST_SECOND);
+
+  /* now with rate effects
+   * Note, they are currently out of sync */
+  _assert_set_rate (rate0, "rate", 0.5, val);
+  _assert_set_rate (rate1, "rate", 2.0, val);
+  _assert_set_rate (rate2, "rate", 4.0, val);
+
+  CHECK_OBJECT_PROPS_MAX (clip, 20 * GST_SECOND, 13 * GST_SECOND,
+      10 * GST_SECOND, 93 * GST_SECOND);
+
+  /* start of the clip is the same */
+  _assert_internal_to_timeline (clip, source0, 13, 20);
+  _assert_internal_to_timeline (clip, source1, 13, 20);
+  _assert_internal_to_timeline (clip, overlay, 7, 20);
+  _assert_timeline_to_internal (clip, source0, 20, 13);
+  _assert_timeline_to_internal (clip, source1, 20, 13);
+  _assert_timeline_to_internal (clip, overlay, 20, 7);
+
+  /* middle is different */
+  /* 5 seconds in the timeline is 2.5 seconds into the source */
+  _assert_internal_to_timeline (clip, source0, 15.5, 25);
+  /* 5 seconds in the timeline is 40 seconds into the source */
+  _assert_internal_to_timeline (clip, source1, 53, 25);
+  /* 5 seconds in the timeline is 20 seconds into the source */
+  _assert_internal_to_timeline (clip, overlay, 27, 25);
+  /* reverse */
+  _assert_timeline_to_internal (clip, source0, 25, 15.5);
+  _assert_timeline_to_internal (clip, source1, 25, 53);
+  _assert_timeline_to_internal (clip, overlay, 25, 27);
+
+  /* end is different */
+  _assert_internal_to_timeline (clip, source0, 18, 30);
+  _assert_internal_to_timeline (clip, source1, 93, 30);
+  _assert_internal_to_timeline (clip, overlay, 47, 30);
+  _assert_timeline_to_internal (clip, source0, 30, 18);
+  _assert_timeline_to_internal (clip, source1, 30, 93);
+  _assert_timeline_to_internal (clip, overlay, 30, 47);
+
+  /* beyond end is different */
+  _assert_internal_to_timeline (clip, source0, 68, 130);
+  _assert_internal_to_timeline (clip, source1, 893, 130);
+  _assert_internal_to_timeline (clip, overlay, 447, 130);
+  _assert_timeline_to_internal (clip, source0, 130, 68);
+  _assert_timeline_to_internal (clip, source1, 130, 893);
+  _assert_timeline_to_internal (clip, overlay, 130, 447);
+
+  /* before the start */
+  _assert_internal_to_timeline (clip, source0, 12.5, 19);
+  _assert_internal_to_timeline (clip, source1, 5, 19);
+  _assert_internal_to_timeline (clip, overlay, 3, 19);
+  _assert_timeline_to_internal (clip, source0, 19, 12.5);
+  _assert_timeline_to_internal (clip, source1, 19, 5);
+  _assert_timeline_to_internal (clip, overlay, 19, 3);
+
+  /* too early for source1 and overlay */
+  _assert_internal_to_timeline (clip, source0, 12, 18);
+  _assert_timeline_to_internal (clip, source0, 18, 12);
+  _assert_timeline_to_internal_fails (clip, source1, 18,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_timeline_to_internal_fails (clip, overlay, 18,
+      GES_ERROR_NEGATIVE_TIME);
+
+  assert_set_inpoint (overlay, 8 * GST_SECOND);
+  /* now fine */
+  _assert_internal_to_timeline (clip, overlay, 0, 18);
+  _assert_timeline_to_internal (clip, overlay, 18, 0);
+
+  assert_set_inpoint (overlay, 7 * GST_SECOND);
+
+  /* still not too early for source0 */
+  _assert_internal_to_timeline (clip, source0, 5.5, 5);
+  _assert_timeline_to_internal (clip, source0, 5, 5.5);
+  _assert_timeline_to_internal_fails (clip, source1, 5,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_timeline_to_internal_fails (clip, overlay, 5,
+      GES_ERROR_NEGATIVE_TIME);
+
+  _assert_internal_to_timeline (clip, source0, 3, 0);
+  _assert_timeline_to_internal (clip, source0, 0, 3);
+  _assert_timeline_to_internal_fails (clip, source1, 5,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_timeline_to_internal_fails (clip, overlay, 5,
+      GES_ERROR_NEGATIVE_TIME);
+
+  /* too early for the timeline */
+  _assert_internal_to_timeline_fails (clip, source0, 2,
+      GES_ERROR_NEGATIVE_TIME);
+
+  /* re-sync rates between tracks */
+  _assert_set_rate (rate2, "rate", 0.25, val);
+
+  CHECK_OBJECT_PROPS_MAX (clip, 20 * GST_SECOND, 13 * GST_SECOND,
+      10 * GST_SECOND, 93 * GST_SECOND);
+
+  /* start of the clip */
+  _assert_internal_to_timeline (clip, source0, 13, 20);
+  _assert_internal_to_timeline (clip, source1, 13, 20);
+  _assert_internal_to_timeline (clip, overlay, 7, 20);
+  _assert_frame_to_timeline (clip, 390, 20);
+  _assert_timeline_to_internal (clip, source0, 20, 13);
+  _assert_timeline_to_internal (clip, source1, 20, 13);
+  _assert_timeline_to_internal (clip, overlay, 20, 7);
+
+  /* middle of the clip */
+  _assert_internal_to_timeline (clip, source0, 15.5, 25);
+  _assert_internal_to_timeline (clip, source1, 15.5, 25);
+  _assert_internal_to_timeline (clip, overlay, 8.25, 25);
+  _assert_frame_to_timeline (clip, 465, 25);
+  _assert_timeline_to_internal (clip, source0, 25, 15.5);
+  _assert_timeline_to_internal (clip, source1, 25, 15.5);
+  _assert_timeline_to_internal (clip, overlay, 25, 8.25);
+
+  /* end of the clip */
+  _assert_internal_to_timeline (clip, source0, 18, 30);
+  _assert_internal_to_timeline (clip, source1, 18, 30);
+  _assert_internal_to_timeline (clip, overlay, 9.5, 30);
+  _assert_frame_to_timeline (clip, 540, 30);
+  _assert_timeline_to_internal (clip, source0, 30, 18);
+  _assert_timeline_to_internal (clip, source1, 30, 18);
+  _assert_timeline_to_internal (clip, overlay, 30, 9.5);
+
+  /* beyond the end of the clip */
+  /* exceeds the max-duration of the elements, but that is ok */
+  _assert_internal_to_timeline (clip, source0, 68, 130);
+  _assert_internal_to_timeline (clip, source1, 68, 130);
+  _assert_internal_to_timeline (clip, overlay, 34.5, 130);
+  _assert_frame_to_timeline (clip, 2040, 130);
+  _assert_timeline_to_internal (clip, source0, 130, 68);
+  _assert_timeline_to_internal (clip, source1, 130, 68);
+  _assert_timeline_to_internal (clip, overlay, 130, 34.5);
+
+  /* before the start of the clip */
+  _assert_internal_to_timeline (clip, source0, 10.5, 15);
+  _assert_internal_to_timeline (clip, source1, 10.5, 15);
+  _assert_internal_to_timeline (clip, overlay, 5.75, 15);
+  _assert_frame_to_timeline (clip, 315, 15);
+  _assert_timeline_to_internal (clip, source0, 15, 10.5);
+  _assert_timeline_to_internal (clip, source1, 15, 10.5);
+  _assert_timeline_to_internal (clip, overlay, 15, 5.75);
+
+  /* not too early */
+  _assert_internal_to_timeline (clip, source0, 3, 0);
+  _assert_internal_to_timeline (clip, source1, 3, 0);
+  _assert_internal_to_timeline (clip, overlay, 2, 0);
+  _assert_frame_to_timeline (clip, 90, 0);
+  _assert_timeline_to_internal (clip, source0, 0, 3);
+  _assert_timeline_to_internal (clip, source1, 0, 3);
+  _assert_timeline_to_internal (clip, overlay, 0, 2);
+
+  /* too early for timeline */
+  _assert_internal_to_timeline_fails (clip, source0, 2,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_internal_to_timeline_fails (clip, source1, 2,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_internal_to_timeline_fails (clip, overlay, 1,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_frame_to_timeline_fails (clip, 89, GES_ERROR_INVALID_FRAME_NUMBER);
+
+  assert_set_start (clip, 30 * GST_SECOND);
+  /* timeline times have shifted by 10 */
+  _assert_timeline_to_internal (clip, source0, 10, 3);
+  _assert_timeline_to_internal (clip, source1, 10, 3);
+  _assert_timeline_to_internal (clip, overlay, 10, 2);
+
+  _assert_timeline_to_internal (clip, source0, 4, 0);
+  _assert_timeline_to_internal (clip, source1, 4, 0);
+  _assert_timeline_to_internal (clip, overlay, 2, 0);
+  /* too early for internal */
+  _assert_timeline_to_internal_fails (clip, source0, 3,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_timeline_to_internal_fails (clip, source1, 3,
+      GES_ERROR_NEGATIVE_TIME);
+  _assert_timeline_to_internal_fails (clip, overlay, 1,
+      GES_ERROR_NEGATIVE_TIME);
+
+  g_value_unset (&val);
+  gst_object_unref (asset);
+  gst_object_unref (timeline);
+
+  ges_deinit ();
 }
 
 GST_END_TEST;
@@ -4813,6 +5230,7 @@ ges_suite (void)
   tcase_add_test (tc_chain, test_children_properties_change);
   tcase_add_test (tc_chain, test_copy_paste_children_properties);
   tcase_add_test (tc_chain, test_unchanged_after_layer_add_failure);
+  tcase_add_test (tc_chain, test_convert_time);
 
   return s;
 }