timeline: Implement snapping to markers
authorPiotrek Brzeziński <thewildtree@outlook.com>
Sun, 20 Jun 2021 21:51:02 +0000 (23:51 +0200)
committerPiotrek Brzeziński <thewildtree@outlook.com>
Sun, 4 Jul 2021 19:08:38 +0000 (21:08 +0200)
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-editing-services/-/merge_requests/259>

ges/ges-enums.c
ges/ges-enums.h
ges/ges-internal.h
ges/ges-marker-list.c
ges/ges-marker-list.h
ges/ges-timeline-tree.c
tests/check/ges/markerlist.c
tests/check/ges/timelineedition.c

index 767b4a0..83b5cd7 100644 (file)
@@ -605,3 +605,26 @@ ges_meta_flag_get_type (void)
   g_once (&once, (GThreadFunc) register_ges_meta_flag, &id);
   return id;
 }
+
+static void
+register_ges_marker_flags (GType * id)
+{
+  static const GFlagsValue values[] = {
+    {C_ENUM (GES_MARKER_FLAG_NONE), "GES_MARKER_FLAG_NONE", "none"},
+    {C_ENUM (GES_MARKER_FLAG_SNAPPABLE), "GES_MARKER_FLAG_SNAPPABLE",
+        "snappable"},
+    {0, NULL, NULL}
+  };
+
+  *id = g_flags_register_static ("GESMarkerFlags", values);
+}
+
+GType
+ges_marker_flags_get_type (void)
+{
+  static GType id;
+  static GOnce once = G_ONCE_INIT;
+
+  g_once (&once, (GThreadFunc) register_ges_marker_flags, &id);
+  return id;
+}
index edcd84e..ff7a013 100644 (file)
@@ -564,6 +564,23 @@ const gchar * ges_edge_name (GESEdge edge);
 GES_API
 GType ges_edge_get_type (void);
 
+#define GES_TYPE_MARKER_FLAGS (ges_marker_flags_get_type ())
+
+GES_API
+GType ges_marker_flags_get_type (void);
+
+/**
+ * GESMarkerFlags:
+ * @GES_MARKER_FLAG_NONE: Marker does not serve any special purpose.
+ * @GES_MARKER_FLAG_SNAPPABLE: Marker can be a snapping target.
+ *
+ * Since: 1.20
+ */
+typedef enum {
+  GES_MARKER_FLAG_NONE = 0,
+  GES_MARKER_FLAG_SNAPPABLE = 1 << 0,
+} GESMarkerFlags;
+
 
 GES_API
 const gchar * ges_track_type_name (GESTrackType type);
index 3200892..dad6c9c 100644 (file)
@@ -586,10 +586,10 @@ G_GNUC_INTERNAL gchar *ges_test_source_asset_check_id         (GType type, const
                                                                GError **error);
 
 /*******************************
- * GESMarkerList serialization *
+ *        GESMarkerList        *
  *******************************/
 
-
+G_GNUC_INTERNAL GESMarker * ges_marker_list_get_closest (GESMarkerList *list, GstClockTime position);
 G_GNUC_INTERNAL gchar * ges_marker_list_serialize (const GValue * v);
 G_GNUC_INTERNAL gboolean ges_marker_list_deserialize (GValue *dest, const gchar *s);
 
index fbab094..d18caf2 100644 (file)
@@ -53,12 +53,12 @@ G_DEFINE_TYPE_WITH_CODE (GESMarker, ges_marker, G_TYPE_OBJECT,
 
 enum
 {
-  PROP_0,
-  PROP_POSITION,
-  PROP_LAST
+  PROP_MARKER_0,
+  PROP_MARKER_POSITION,
+  PROP_MARKER_LAST
 };
 
-static GParamSpec *properties[PROP_LAST];
+static GParamSpec *marker_properties[PROP_MARKER_LAST];
 
 /* GObject Standard vmethods*/
 static void
@@ -68,7 +68,7 @@ ges_marker_get_property (GObject * object, guint property_id,
   GESMarker *marker = GES_MARKER (object);
 
   switch (property_id) {
-    case PROP_POSITION:
+    case PROP_MARKER_POSITION:
       g_value_set_uint64 (value, marker->position);
       break;
     default:
@@ -97,12 +97,12 @@ ges_marker_class_init (GESMarkerClass * klass)
    *
    * Since: 1.18
    */
-  properties[PROP_POSITION] =
+  marker_properties[PROP_MARKER_POSITION] =
       g_param_spec_uint64 ("position", "Position",
       "The position of the marker", 0, G_MAXUINT64,
       GST_CLOCK_TIME_NONE, G_PARAM_READABLE);
-  g_object_class_install_property (object_class, PROP_POSITION,
-      properties[PROP_POSITION]);
+  g_object_class_install_property (object_class, PROP_MARKER_POSITION,
+      marker_properties[PROP_MARKER_POSITION]);
 
 }
 
@@ -119,10 +119,20 @@ struct _GESMarkerList
 
   GSequence *markers;
   GHashTable *markers_iters;
+  GESMarkerFlags flags;
 };
 
 enum
 {
+  PROP_MARKER_LIST_0,
+  PROP_MARKER_LIST_FLAGS,
+  PROP_MARKER_LIST_LAST
+};
+
+static GParamSpec *list_properties[PROP_MARKER_LIST_LAST];
+
+enum
+{
   MARKER_ADDED,
   MARKER_REMOVED,
   MARKER_MOVED,
@@ -134,6 +144,37 @@ static guint ges_marker_list_signals[LAST_SIGNAL] = { 0 };
 G_DEFINE_TYPE (GESMarkerList, ges_marker_list, G_TYPE_OBJECT);
 
 static void
+ges_marker_list_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  GESMarkerList *self = GES_MARKER_LIST (object);
+
+  switch (property_id) {
+    case PROP_MARKER_LIST_FLAGS:
+      g_value_set_flags (value, self->flags);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+  }
+}
+
+static void
+ges_marker_list_set_property (GObject * object,
+    guint property_id, const GValue * value, GParamSpec * pspec)
+{
+  GESMarkerList *self = GES_MARKER_LIST (object);
+
+  switch (property_id) {
+    case PROP_MARKER_LIST_FLAGS:
+      self->flags = g_value_get_flags (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+  }
+}
+
+static void
 remove_marker (gpointer data)
 {
   GESMarker *marker = (GESMarker *) data;
@@ -165,6 +206,23 @@ ges_marker_list_class_init (GESMarkerListClass * klass)
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
   object_class->finalize = ges_marker_list_finalize;
+  object_class->get_property = ges_marker_list_get_property;
+  object_class->set_property = ges_marker_list_set_property;
+
+/**
+  * GESMarkerList:flags:
+  *
+  * Flags indicating how markers on the list should be treated.
+  *
+  * Since: 1.20
+  */
+  list_properties[PROP_MARKER_LIST_FLAGS] =
+      g_param_spec_flags ("flags", "Flags",
+      "Functionalities the marker list should be used for",
+      GES_TYPE_MARKER_FLAGS, GES_MARKER_FLAG_NONE,
+      G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+  g_object_class_install_property (object_class, PROP_MARKER_LIST_FLAGS,
+      list_properties[PROP_MARKER_LIST_FLAGS]);
 
 /**
   * GESMarkerList::marker-added:
@@ -317,7 +375,6 @@ done:
   return ret;
 }
 
-
 /**
  * ges_marker_list_get_markers:
  *
@@ -347,6 +404,52 @@ ges_marker_list_get_markers (GESMarkerList * self)
   return ret;
 }
 
+/*
+ * ges_marker_list_get_closest:
+ * @position: The position which we want to find the closest marker to
+ *
+ * Returns: (transfer full): The marker found to be the closest
+ * to the given position. If two markers are at equal distance from position,
+ * the "earlier" one will be returned.
+ */
+GESMarker *
+ges_marker_list_get_closest (GESMarkerList * self, GstClockTime position)
+{
+  GESMarker *new_marker, *ret = NULL;
+  GstClockTime distance_next, distance_prev;
+  GSequenceIter *iter;
+
+  if (g_sequence_is_empty (self->markers))
+    goto done;
+
+  new_marker = (GESMarker *) g_object_new (GES_TYPE_MARKER, NULL);
+  new_marker->position = position;
+  iter = g_sequence_search (self->markers, new_marker, cmp_marker, NULL);
+  g_object_unref (new_marker);
+
+  if (g_sequence_iter_is_begin (iter)) {
+    /* We know the sequence isn't empty, this is safe */
+    ret = g_sequence_get (iter);
+  } else if (g_sequence_iter_is_end (iter)) {
+    /* We know the sequence isn't empty, this is safe */
+    ret = g_sequence_get (g_sequence_iter_prev (iter));
+  } else {
+    GESMarker *next_marker, *prev_marker;
+
+    prev_marker = g_sequence_get (g_sequence_iter_prev (iter));
+    next_marker = g_sequence_get (iter);
+
+    distance_next = next_marker->position - position;
+    distance_prev = position - prev_marker->position;
+
+    ret = distance_prev <= distance_next ? prev_marker : next_marker;
+  }
+
+done:
+  if (ret)
+    return g_object_ref (ret);
+  return NULL;
+}
 
 /**
  * ges_marker_list_move:
index 3960755..51c7ed1 100644 (file)
@@ -62,7 +62,7 @@ guint ges_marker_list_size (GESMarkerList * list);
 
 
 GES_API
-GList *ges_marker_list_get_markers (GESMarkerList *list);
+GList * ges_marker_list_get_markers (GESMarkerList *list);
 
 GES_API
 gboolean ges_marker_list_move (GESMarkerList *list, GESMarker *marker, GstClockTime position);
index 423eef9..4f00c9a 100644 (file)
@@ -24,6 +24,7 @@
 
 #include "ges-timeline-tree.h"
 #include "ges-internal.h"
+#include "ges-marker-list.h"
 
 GST_DEBUG_CATEGORY_STATIC (tree_debug);
 #undef GST_CAT_DEFAULT
@@ -401,6 +402,28 @@ get_start_end_from_offset (GESTimelineElement * element, ElementEditMode mode,
  ****************************************************/
 
 static void
+snap_to_marker (GESTrackElement * element, GstClockTime position,
+    gboolean negative, GstClockTime marker_timestamp,
+    GESTrackElement * marker_parent, SnappedPosition * snap)
+{
+  GstClockTime distance;
+
+  if (negative)
+    distance = _clock_time_plus (position, marker_timestamp);
+  else
+    distance = _abs_clock_time_distance (position, marker_timestamp);
+
+  if (GST_CLOCK_TIME_IS_VALID (distance) && distance <= snap->distance) {
+    snap->negative = negative;
+    snap->position = position;
+    snap->distance = distance;
+    snap->snapped = marker_timestamp;
+    snap->element = element;
+    snap->snapped_to = marker_parent;
+  }
+}
+
+static void
 snap_to_edge (GESTrackElement * element, GstClockTime position,
     gboolean negative, GESTrackElement * snap_to, GESEdge edge,
     SnappedPosition * snap)
@@ -431,6 +454,51 @@ snap_to_edge (GESTrackElement * element, GstClockTime position,
   }
 }
 
+static void
+find_marker_snap (const GESMetaContainer * container, const gchar * key,
+    const GValue * value, TreeIterationData * data)
+{
+  GESTrackElement *marker_parent, *moving;
+  GESClip *parent_clip;
+  GstClockTime timestamp;
+  GESMarkerList *marker_list;
+  GESMarker *marker;
+  GESMarkerFlags flags;
+  gpointer gvalue = g_value_get_object (value);
+
+  if (!GES_IS_MARKER_LIST (gvalue))
+    return;
+
+  marker_list = GES_MARKER_LIST (gvalue);
+
+  g_object_get (marker_list, "flags", &flags, NULL);
+  if (!(flags & GES_MARKER_FLAG_SNAPPABLE))
+    return;
+
+  marker_parent = GES_TRACK_ELEMENT ((gpointer) container);
+  moving = GES_TRACK_ELEMENT (data->element);
+  parent_clip = (GESClip *) GES_TIMELINE_ELEMENT_PARENT (marker_parent);
+
+  /* Translate current position into the target clip's time domain */
+  timestamp =
+      ges_clip_get_internal_time_from_timeline_time (parent_clip, marker_parent,
+      data->position, NULL);
+  marker = ges_marker_list_get_closest (marker_list, timestamp);
+
+  if (marker == NULL)
+    return;
+
+  /* Make timestamp timeline-relative again */
+  g_object_get (marker, "position", &timestamp, NULL);
+  timestamp =
+      ges_clip_get_timeline_time_from_internal_time (parent_clip, marker_parent,
+      timestamp, NULL);
+  snap_to_marker (moving, data->position, data->negative, timestamp,
+      marker_parent, data->snap);
+
+  g_object_unref (marker);
+}
+
 static gboolean
 find_snap (GNode * node, TreeIterationData * data)
 {
@@ -454,6 +522,9 @@ find_snap (GNode * node, TreeIterationData * data)
   snap_to_edge (moving, data->position, data->negative, track_el,
       GES_EDGE_START, data->snap);
 
+  ges_meta_container_foreach (GES_META_CONTAINER (element),
+      (GESMetaForeachFunc) find_marker_snap, data);
+
   return FALSE;
 }
 
index 7649e0a..9e79c9a 100644 (file)
@@ -453,7 +453,6 @@ GST_START_TEST (test_marker_color)
 
 GST_END_TEST;
 
-
 static Suite *
 ges_suite (void)
 {
index 24790a1..2d4e5dd 100644 (file)
@@ -398,7 +398,7 @@ GST_START_TEST (test_snapping)
    *                        30-------+0-------------+
    * inpoints               5  clip  ||  clip2      |-------------+
    *                        +------- 62 -----------122  clip1     |
-   * time                                           +------------132 
+   * time                                           +------------132
    * Check that clip1 snaps with the end of clip2 */
   fail_unless (ges_container_edit (clip1, NULL, -1, GES_EDIT_MODE_NORMAL,
           GES_EDGE_NONE, 125) == TRUE);
@@ -1139,6 +1139,142 @@ GST_START_TEST (test_snapping_groups)
 
 GST_END_TEST;
 
+GST_START_TEST (test_marker_snapping)
+{
+  GESTrack *track;
+  GESTimeline *timeline;
+  GESTrackElement *trackelement1, *trackelement2;
+  GESContainer *clip1, *clip2;
+  GESLayer *layer;
+  GList *trackelements;
+  GESMarkerList *marker_list1, *marker_list2;
+
+  ges_init ();
+
+  track = GES_TRACK (ges_video_track_new ());
+  fail_unless (track != NULL);
+
+  timeline = ges_timeline_new ();
+  fail_unless (timeline != NULL);
+
+  fail_unless (ges_timeline_add_track (timeline, track));
+
+  clip1 = GES_CONTAINER (ges_test_clip_new ());
+  clip2 = GES_CONTAINER (ges_test_clip_new ());
+
+  fail_unless (clip1 && clip2);
+
+  /**
+   * Our timeline
+   * ------------
+   *               30
+   * markers  -----|----------------
+   *          |  clip1  ||  clip2  |
+   * time    20 ------- 50 ------ 110
+   *
+   */
+  g_object_set (clip1, "start", (guint64) 20, "duration", (guint64) 30,
+      "in-point", (guint64) 0, NULL);
+  g_object_set (clip2, "start", (guint64) 50, "duration", (guint64) 60,
+      "in-point", (guint64) 0, NULL);
+
+  fail_unless ((layer = ges_timeline_append_layer (timeline)) != NULL);
+  assert_equals_int (ges_layer_get_priority (layer), 0);
+
+  fail_unless (ges_layer_add_clip (layer, GES_CLIP (clip1)));
+  fail_unless ((trackelements = GES_CONTAINER_CHILDREN (clip1)) != NULL);
+  fail_unless ((trackelement1 =
+          GES_TRACK_ELEMENT (trackelements->data)) != NULL);
+  fail_unless (ges_track_element_get_track (trackelement1) == track);
+
+  fail_unless (ges_layer_add_clip (layer, GES_CLIP (clip2)));
+  fail_unless ((trackelements = GES_CONTAINER_CHILDREN (clip2)) != NULL);
+  fail_unless ((trackelement2 =
+          GES_TRACK_ELEMENT (trackelements->data)) != NULL);
+  fail_unless (ges_track_element_get_track (trackelement2) == track);
+
+  marker_list1 = ges_marker_list_new ();
+  g_object_set (marker_list1, "flags", GES_MARKER_FLAG_SNAPPABLE, NULL);
+  ges_marker_list_add (marker_list1, 10);
+  ges_marker_list_add (marker_list1, 20);
+  fail_unless (ges_meta_container_set_marker_list (GES_META_CONTAINER
+          (trackelement1), "ges-test", marker_list1));
+
+  /**
+   * Snapping clip2 to a marker on clip1
+   * ------------
+   *               30 40
+   * markers  -----|--|--
+   *          |  clip1  |
+   * time    20 ------ 50
+   *              -----------
+   *              |  clip2  |
+   *             30 ------ 90
+   */
+  g_object_set (timeline, "snapping-distance", (guint64) 3, NULL);
+  /* Move within 2 units of marker timestamp */
+  fail_unless (ges_container_edit (clip2, NULL, -1, GES_EDIT_MODE_NORMAL,
+          GES_EDGE_NONE, 32) == TRUE);
+  /* Clip nr. 2 should snap to marker at timestamp 30 */
+  DEEP_CHECK (clip1, 20, 0, 30);
+  DEEP_CHECK (clip2, 30, 0, 60);
+
+  /**
+   * Snapping clip1 to a marker on clip2
+   * ------------
+   *                           90
+   * markers                 --|--------
+   *                         |  clip1  |
+   * time                   80 ------ 110
+   * markers      ----------|--
+   *              |   clip2   |
+   *             30 -------- 90
+   */
+  marker_list2 = ges_marker_list_new ();
+  g_object_set (marker_list2, "flags", GES_MARKER_FLAG_SNAPPABLE, NULL);
+  ges_marker_list_add (marker_list2, 40);
+  ges_marker_list_add (marker_list2, 50);
+  fail_unless (ges_meta_container_set_marker_list (GES_META_CONTAINER
+          (trackelement2), "ges-test", marker_list2));
+
+  fail_unless (ges_container_edit (clip1, NULL, -1, GES_EDIT_MODE_NORMAL,
+          GES_EDGE_NONE, 77) == TRUE);
+  DEEP_CHECK (clip1, 80, 0, 30);
+  DEEP_CHECK (clip2, 30, 0, 60);
+
+  /**
+   * Checking if clip's own markers are properly ignored when snapping
+   * (moving clip1 close to where one of its markers is)
+   * ------------
+   *                     100     112     122
+   * markers              |     --|-------|--
+   *                old m.pos.  |   clip1   |
+   * time                      102 ------- 132
+   */
+  fail_unless (ges_container_edit (clip1, NULL, -1, GES_EDIT_MODE_NORMAL,
+          GES_EDGE_NONE, 102) == TRUE);
+  DEEP_CHECK (clip1, 102, 0, 30);
+  DEEP_CHECK (clip2, 30, 0, 60);
+
+  /**
+   * Checking if non-snappable marker lists are correctly ignored.
+   * (moving clip1 close to clip2's non-snappable marker)
+   */
+  g_object_set (marker_list2, "flags", GES_MARKER_FLAG_NONE, NULL);
+  fail_unless (ges_container_edit (clip1, NULL, -1, GES_EDIT_MODE_NORMAL,
+          GES_EDGE_NONE, 82) == TRUE);
+  DEEP_CHECK (clip1, 82, 0, 30);
+  DEEP_CHECK (clip2, 30, 0, 60);
+
+  g_object_unref (marker_list1);
+  g_object_unref (marker_list2);
+  check_destroyed (G_OBJECT (timeline), G_OBJECT (trackelement1),
+      trackelement2, clip1, clip2, layer, marker_list1, marker_list2, NULL);
+  ges_deinit ();
+}
+
+GST_END_TEST;
+
 static Suite *
 ges_suite (void)
 {
@@ -1153,6 +1289,7 @@ ges_suite (void)
   tcase_add_test (tc_chain, test_simple_triming);
   tcase_add_test (tc_chain, test_groups);
   tcase_add_test (tc_chain, test_snapping_groups);
+  tcase_add_test (tc_chain, test_marker_snapping);
 
   return s;
 }