timeline: Implement snapping to markers

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-editing-services/-/merge_requests/259>
This commit is contained in:
Piotrek Brzeziński 2021-06-20 23:51:02 +02:00
parent 777be5fad9
commit 3e66d05ed2
8 changed files with 364 additions and 14 deletions

View 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;
}

View 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);

View 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);

View 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,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:

View 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);

View 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
@ -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", &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;
}

View file

@ -453,7 +453,6 @@ GST_START_TEST (test_marker_color)
GST_END_TEST;
static Suite *
ges_suite (void)
{

View 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;
}