Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-editing-services/-/merge_requests/259>
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;
+}
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);
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);
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
GESMarker *marker = GES_MARKER (object);
switch (property_id) {
- case PROP_POSITION:
+ case PROP_MARKER_POSITION:
g_value_set_uint64 (value, marker->position);
break;
default:
*
* 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]);
}
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,
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;
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:
return ret;
}
-
/**
* ges_marker_list_get_markers:
*
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:
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);
#include "ges-timeline-tree.h"
#include "ges-internal.h"
+#include "ges-marker-list.h"
GST_DEBUG_CATEGORY_STATIC (tree_debug);
#undef GST_CAT_DEFAULT
****************************************************/
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)
}
}
+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)
{
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;
}
GST_END_TEST;
-
static Suite *
ges_suite (void)
{
* 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);
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)
{
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;
}