From 3e66d05ed26a43e2706bd222f0572deeaae7d015 Mon Sep 17 00:00:00 2001 From: Piotrek BrzeziƄski Date: Sun, 20 Jun 2021 23:51:02 +0200 Subject: timeline: Implement snapping to markers Part-of: --- ges/ges-enums.c | 23 +++++++ ges/ges-enums.h | 17 +++++ ges/ges-internal.h | 4 +- ges/ges-marker-list.c | 121 ++++++++++++++++++++++++++++++--- ges/ges-marker-list.h | 2 +- ges/ges-timeline-tree.c | 71 +++++++++++++++++++ tests/check/ges/markerlist.c | 1 - tests/check/ges/timelineedition.c | 139 +++++++++++++++++++++++++++++++++++++- 8 files changed, 364 insertions(+), 14 deletions(-) diff --git a/ges/ges-enums.c b/ges/ges-enums.c index 767b4a09..83b5cd79 100644 --- a/ges/ges-enums.c +++ b/ges/ges-enums.c @@ -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; +} diff --git a/ges/ges-enums.h b/ges/ges-enums.h index edcd84e2..ff7a0136 100644 --- a/ges/ges-enums.h +++ b/ges/ges-enums.h @@ -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); diff --git a/ges/ges-internal.h b/ges/ges-internal.h index 3200892e..dad6c9cd 100644 --- a/ges/ges-internal.h +++ b/ges/ges-internal.h @@ -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); diff --git a/ges/ges-marker-list.c b/ges/ges-marker-list.c index fbab094e..d18caf25 100644 --- a/ges/ges-marker-list.c +++ b/ges/ges-marker-list.c @@ -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,8 +119,18 @@ 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, @@ -133,6 +143,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) { @@ -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: diff --git a/ges/ges-marker-list.h b/ges/ges-marker-list.h index 39607559..51c7ed14 100644 --- a/ges/ges-marker-list.h +++ b/ges/ges-marker-list.h @@ -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); diff --git a/ges/ges-timeline-tree.c b/ges/ges-timeline-tree.c index 423eef93..4f00c9af 100644 --- a/ges/ges-timeline-tree.c +++ b/ges/ges-timeline-tree.c @@ -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 @@ -400,6 +401,28 @@ get_start_end_from_offset (GESTimelineElement * element, ElementEditMode mode, * Snapping * ****************************************************/ +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, @@ -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", ×tamp, 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; } diff --git a/tests/check/ges/markerlist.c b/tests/check/ges/markerlist.c index 7649e0ab..9e79c9a0 100644 --- a/tests/check/ges/markerlist.c +++ b/tests/check/ges/markerlist.c @@ -453,7 +453,6 @@ GST_START_TEST (test_marker_color) GST_END_TEST; - static Suite * ges_suite (void) { diff --git a/tests/check/ges/timelineedition.c b/tests/check/ges/timelineedition.c index 24790a1a..2d4e5dd7 100644 --- a/tests/check/ges/timelineedition.c +++ b/tests/check/ges/timelineedition.c @@ -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; } -- cgit v1.2.3