2585 lines
		
	
	
		
			79 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			2585 lines
		
	
	
		
			79 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /* GStreamer Editing Services
 | |
|  * Copyright (C) 2019 Igalia S.L
 | |
|  *     Author: 2019 Thibault Saunier <tsaunier@igalia.com>
 | |
|  *
 | |
|  * This library is free software; you can redistribute it and/or
 | |
|  * modify it under the terms of the GNU Library General Public
 | |
|  * License as published by the Free Software Foundation; either
 | |
|  * version 2 of the License, or (at your option) any later version.
 | |
|  *
 | |
|  * This library is distributed in the hope that it will be useful,
 | |
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | |
|  * Library General Public License for more details.
 | |
|  *
 | |
|  * You should have received a copy of the GNU Library General Public
 | |
|  * License along with this library; if not, write to the
 | |
|  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 | |
|  * Boston, MA 02110-1301, USA.
 | |
|  */
 | |
| 
 | |
| #ifdef HAVE_CONFIG_H
 | |
| #include "config.h"
 | |
| #endif
 | |
| 
 | |
| #include "ges-timeline-tree.h"
 | |
| #include "ges-internal.h"
 | |
| #include "ges-marker-list.h"
 | |
| 
 | |
| GST_DEBUG_CATEGORY_STATIC (tree_debug);
 | |
| #undef GST_CAT_DEFAULT
 | |
| #define GST_CAT_DEFAULT tree_debug
 | |
| 
 | |
| #define ELEMENT_EDGE_VALUE(e, edge) ((edge == GES_EDGE_END) ? _END (e) : _START (e))
 | |
| 
 | |
| typedef struct _SnappedPosition
 | |
| {
 | |
|   /* the element that was being snapped */
 | |
|   GESTrackElement *element;
 | |
|   /* the position of element, and whether it is a negative position */
 | |
|   gboolean negative;
 | |
|   GstClockTime position;
 | |
|   /* the element that was snapped to */
 | |
|   GESTrackElement *snapped_to;
 | |
|   /* the snapped positioned */
 | |
|   GstClockTime snapped;
 | |
|   /* the distance below which two elements can snap */
 | |
|   GstClockTime distance;
 | |
| } SnappedPosition;
 | |
| 
 | |
| typedef enum
 | |
| {
 | |
|   EDIT_MOVE,
 | |
|   EDIT_TRIM_START,
 | |
|   EDIT_TRIM_END,
 | |
|   EDIT_TRIM_INPOINT_ONLY,
 | |
| } ElementEditMode;
 | |
| 
 | |
| typedef struct _EditData
 | |
| {
 | |
|   /* offsets to use */
 | |
|   GstClockTime offset;
 | |
|   gint64 layer_offset;
 | |
|   /* actual values */
 | |
|   GstClockTime duration;
 | |
|   GstClockTime start;
 | |
|   GstClockTime inpoint;
 | |
|   guint32 layer_priority;
 | |
|   /* mode */
 | |
|   ElementEditMode mode;
 | |
| } EditData;
 | |
| 
 | |
| typedef struct _PositionData
 | |
| {
 | |
|   guint32 layer_priority;
 | |
|   GstClockTime start;
 | |
|   GstClockTime end;
 | |
| } PositionData;
 | |
| 
 | |
| /*  *INDENT-OFF* */
 | |
| struct _TreeIterationData
 | |
| {
 | |
|   GNode *root;
 | |
|   gboolean res;
 | |
|   /* an error to set */
 | |
|   GError **error;
 | |
| 
 | |
|   /* The element we are visiting */
 | |
|   GESTimelineElement *element;
 | |
|   /* the position data of the visited element */
 | |
|   PositionData *pos_data;
 | |
| 
 | |
|   /* All the TrackElement currently moving: owned by data */
 | |
|   GHashTable *moving;
 | |
| 
 | |
|   /* Elements overlaping on the start/end of @element */
 | |
|   GESTimelineElement *overlaping_on_start;
 | |
|   GESTimelineElement *overlaping_on_end;
 | |
|   GstClockTime overlap_start_final_time;
 | |
|   GstClockTime overlap_end_first_time;
 | |
| 
 | |
|   SnappedPosition *snap;
 | |
|   GList *sources;
 | |
|   GstClockTime position;
 | |
|   GstClockTime negative;
 | |
| 
 | |
|   GESEdge edge;
 | |
|   GList *neighbours;
 | |
| } tree_iteration_data_init = {
 | |
|    .root = NULL,
 | |
|    .res = TRUE,
 | |
|    .element = NULL,
 | |
|    .pos_data = NULL,
 | |
|    .moving = NULL,
 | |
|    .overlaping_on_start = NULL,
 | |
|    .overlaping_on_end = NULL,
 | |
|    .overlap_start_final_time = GST_CLOCK_TIME_NONE,
 | |
|    .overlap_end_first_time = GST_CLOCK_TIME_NONE,
 | |
|    .snap = NULL,
 | |
|    .sources = NULL,
 | |
|    .position = GST_CLOCK_TIME_NONE,
 | |
|    .negative = FALSE,
 | |
|    .edge = GES_EDGE_NONE,
 | |
|    .neighbours = NULL,
 | |
| };
 | |
| /*  *INDENT-ON* */
 | |
| 
 | |
| typedef struct _TreeIterationData TreeIterationData;
 | |
| 
 | |
| static EditData *
 | |
| new_edit_data (ElementEditMode mode, GstClockTimeDiff offset,
 | |
|     gint64 layer_offset)
 | |
| {
 | |
|   EditData *data = g_new (EditData, 1);
 | |
| 
 | |
|   data->start = GST_CLOCK_TIME_NONE;
 | |
|   data->duration = GST_CLOCK_TIME_NONE;
 | |
|   data->inpoint = GST_CLOCK_TIME_NONE;
 | |
|   data->layer_priority = GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY;
 | |
| 
 | |
|   data->mode = mode;
 | |
|   data->offset = offset;
 | |
|   data->layer_offset = layer_offset;
 | |
| 
 | |
|   return data;
 | |
| }
 | |
| 
 | |
| static SnappedPosition *
 | |
| new_snapped_position (GstClockTime distance)
 | |
| {
 | |
|   SnappedPosition *snap;
 | |
| 
 | |
|   if (distance == 0)
 | |
|     return NULL;
 | |
| 
 | |
|   snap = g_new0 (SnappedPosition, 1);
 | |
|   snap->position = GST_CLOCK_TIME_NONE;
 | |
|   snap->snapped = GST_CLOCK_TIME_NONE;
 | |
|   snap->distance = distance;
 | |
| 
 | |
|   return snap;
 | |
| }
 | |
| 
 | |
| static GHashTable *
 | |
| new_edit_table (void)
 | |
| {
 | |
|   return g_hash_table_new_full (NULL, NULL, NULL, g_free);
 | |
| }
 | |
| 
 | |
| static GHashTable *
 | |
| new_position_table (void)
 | |
| {
 | |
|   return g_hash_table_new_full (NULL, NULL, NULL, g_free);
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_init_debug (void)
 | |
| {
 | |
|   GST_DEBUG_CATEGORY_INIT (tree_debug, "gestree",
 | |
|       GST_DEBUG_FG_YELLOW, "timeline tree");
 | |
| }
 | |
| 
 | |
| 
 | |
| static gboolean
 | |
| print_node (GNode * node, gpointer unused_data)
 | |
| {
 | |
|   if (G_NODE_IS_ROOT (node)) {
 | |
|     gst_print ("Timeline: %p\n", node->data);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   gst_print ("%*c- %" GES_FORMAT " - layer %" G_GINT32_FORMAT "\n",
 | |
|       2 * g_node_depth (node), ' ', GES_ARGS (node->data),
 | |
|       ges_timeline_element_get_layer_priority (node->data));
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_debug (GNode * root)
 | |
| {
 | |
|   g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
 | |
|       (GNodeTraverseFunc) print_node, NULL);
 | |
| }
 | |
| 
 | |
| static GNode *
 | |
| find_node (GNode * root, gpointer element)
 | |
| {
 | |
|   return g_node_find (root, G_IN_ORDER, G_TRAVERSE_ALL, element);
 | |
| }
 | |
| 
 | |
| static void
 | |
| timeline_element_parent_cb (GESTimelineElement * child, GParamSpec * arg
 | |
|     G_GNUC_UNUSED, GNode * root)
 | |
| {
 | |
|   GNode *new_parent_node = NULL, *node = find_node (root, child);
 | |
| 
 | |
|   if (child->parent)
 | |
|     new_parent_node = find_node (root, child->parent);
 | |
| 
 | |
|   if (!new_parent_node)
 | |
|     new_parent_node = root;
 | |
| 
 | |
|   g_node_unlink (node);
 | |
|   g_node_prepend (new_parent_node, node);
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_track_element (GNode * root, GESTimelineElement * element)
 | |
| {
 | |
|   GNode *node;
 | |
|   GNode *parent;
 | |
|   GESTimelineElement *toplevel;
 | |
| 
 | |
|   if (find_node (root, element)) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   g_signal_connect (element, "notify::parent",
 | |
|       G_CALLBACK (timeline_element_parent_cb), root);
 | |
| 
 | |
|   toplevel = ges_timeline_element_peak_toplevel (element);
 | |
|   if (toplevel == element) {
 | |
|     GST_DEBUG ("Tracking toplevel element %" GES_FORMAT, GES_ARGS (element));
 | |
| 
 | |
|     node = g_node_prepend_data (root, element);
 | |
|   } else {
 | |
|     parent = find_node (root, element->parent);
 | |
|     GST_LOG ("%" GES_FORMAT "parent is %" GES_FORMAT, GES_ARGS (element),
 | |
|         GES_ARGS (element->parent));
 | |
|     g_assert (parent);
 | |
|     node = g_node_prepend_data (parent, element);
 | |
|   }
 | |
| 
 | |
|   if (GES_IS_CONTAINER (element)) {
 | |
|     GList *tmp;
 | |
| 
 | |
|     for (tmp = GES_CONTAINER_CHILDREN (element); tmp; tmp = tmp->next) {
 | |
|       GNode *child_node = find_node (root, tmp->data);
 | |
| 
 | |
|       if (child_node) {
 | |
|         g_node_unlink (child_node);
 | |
|         g_node_prepend (node, child_node);
 | |
|       } else {
 | |
|         timeline_tree_track_element (root, tmp->data);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   timeline_update_duration (root->data);
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_stop_tracking_element (GNode * root, GESTimelineElement * element)
 | |
| {
 | |
|   GNode *node = find_node (root, element);
 | |
| 
 | |
|   node = find_node (root, element);
 | |
| 
 | |
|   /* Move children to the parent */
 | |
|   while (node->children) {
 | |
|     GNode *tmp = node->children;
 | |
|     g_node_unlink (tmp);
 | |
|     g_node_prepend (node->parent, tmp);
 | |
|   }
 | |
| 
 | |
|   g_assert (node);
 | |
|   GST_DEBUG ("Stop tracking %" GES_FORMAT, GES_ARGS (element));
 | |
|   g_signal_handlers_disconnect_by_func (element, timeline_element_parent_cb,
 | |
|       root);
 | |
| 
 | |
|   g_node_destroy (node);
 | |
|   timeline_update_duration (root->data);
 | |
| }
 | |
| 
 | |
| /****************************************************
 | |
|  *     GstClockTime with over/underflow checking    *
 | |
|  ****************************************************/
 | |
| 
 | |
| static GstClockTime
 | |
| _clock_time_plus (GstClockTime time, GstClockTime add)
 | |
| {
 | |
|   if (!GST_CLOCK_TIME_IS_VALID (time) || !GST_CLOCK_TIME_IS_VALID (add))
 | |
|     return GST_CLOCK_TIME_NONE;
 | |
| 
 | |
|   if (time >= (G_MAXUINT64 - add)) {
 | |
|     GST_ERROR ("The time %" G_GUINT64_FORMAT " would overflow when "
 | |
|         "adding %" G_GUINT64_FORMAT, time, add);
 | |
|     return GST_CLOCK_TIME_NONE;
 | |
|   }
 | |
|   return time + add;
 | |
| }
 | |
| 
 | |
| static GstClockTime
 | |
| _clock_time_minus (GstClockTime time, GstClockTime minus, gboolean * negative)
 | |
| {
 | |
|   if (negative)
 | |
|     *negative = FALSE;
 | |
| 
 | |
|   if (!GST_CLOCK_TIME_IS_VALID (time) || !GST_CLOCK_TIME_IS_VALID (minus))
 | |
|     return GST_CLOCK_TIME_NONE;
 | |
| 
 | |
|   if (time < minus) {
 | |
|     if (negative) {
 | |
|       *negative = TRUE;
 | |
|       return minus - time;
 | |
|     }
 | |
|     /* otherwise don't allow negative */
 | |
|     GST_INFO ("The time %" G_GUINT64_FORMAT " would underflow when "
 | |
|         "subtracting %" G_GUINT64_FORMAT, time, minus);
 | |
|     return GST_CLOCK_TIME_NONE;
 | |
|   }
 | |
|   return time - minus;
 | |
| }
 | |
| 
 | |
| static GstClockTime
 | |
| _clock_time_minus_diff (GstClockTime time, GstClockTimeDiff diff,
 | |
|     gboolean * negative)
 | |
| {
 | |
|   if (negative)
 | |
|     *negative = FALSE;
 | |
| 
 | |
|   if (!GST_CLOCK_TIME_IS_VALID (time))
 | |
|     return GST_CLOCK_TIME_NONE;
 | |
| 
 | |
|   if (diff < 0)
 | |
|     return _clock_time_plus (time, -diff);
 | |
|   else
 | |
|     return _clock_time_minus (time, diff, negative);
 | |
| }
 | |
| 
 | |
| static GstClockTime
 | |
| _abs_clock_time_distance (GstClockTime time1, GstClockTime time2)
 | |
| {
 | |
|   if (!GST_CLOCK_TIME_IS_VALID (time1) || !GST_CLOCK_TIME_IS_VALID (time2))
 | |
|     return GST_CLOCK_TIME_NONE;
 | |
|   if (time1 > time2)
 | |
|     return time1 - time2;
 | |
|   else
 | |
|     return time2 - time1;
 | |
| }
 | |
| 
 | |
| static void
 | |
| get_start_end_from_offset (GESTimelineElement * element, ElementEditMode mode,
 | |
|     GstClockTimeDiff offset, GstClockTime * start, gboolean * negative_start,
 | |
|     GstClockTime * end, gboolean * negative_end)
 | |
| {
 | |
|   GstClockTime current_end =
 | |
|       _clock_time_plus (element->start, element->duration);
 | |
|   GstClockTime new_start = GST_CLOCK_TIME_NONE, new_end = GST_CLOCK_TIME_NONE;
 | |
| 
 | |
|   switch (mode) {
 | |
|     case EDIT_MOVE:
 | |
|       new_start =
 | |
|           _clock_time_minus_diff (element->start, offset, negative_start);
 | |
|       new_end = _clock_time_minus_diff (current_end, offset, negative_end);
 | |
|       break;
 | |
|     case EDIT_TRIM_START:
 | |
|       new_start =
 | |
|           _clock_time_minus_diff (element->start, offset, negative_start);
 | |
|       new_end = current_end;
 | |
|       if (negative_end)
 | |
|         *negative_end = FALSE;
 | |
|       break;
 | |
|     case EDIT_TRIM_END:
 | |
|       new_start = element->start;
 | |
|       if (negative_start)
 | |
|         *negative_start = FALSE;
 | |
|       new_end = _clock_time_minus_diff (current_end, offset, negative_end);
 | |
|       break;
 | |
|     case EDIT_TRIM_INPOINT_ONLY:
 | |
|       GST_ERROR_OBJECT (element, "Trim in-point only not handled");
 | |
|       break;
 | |
|   }
 | |
|   if (start)
 | |
|     *start = new_start;
 | |
|   if (end)
 | |
|     *end = new_end;
 | |
| }
 | |
| 
 | |
| /****************************************************
 | |
|  *                   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,
 | |
|     SnappedPosition * snap)
 | |
| {
 | |
|   GstClockTime edge_pos = ELEMENT_EDGE_VALUE (snap_to, edge);
 | |
|   GstClockTime distance;
 | |
| 
 | |
|   if (negative)
 | |
|     distance = _clock_time_plus (position, edge_pos);
 | |
|   else
 | |
|     distance = _abs_clock_time_distance (position, edge_pos);
 | |
| 
 | |
|   if (GST_CLOCK_TIME_IS_VALID (distance) && distance <= snap->distance) {
 | |
|     GESTimelineElement *parent = GES_TIMELINE_ELEMENT_PARENT (element);
 | |
|     GESTimelineElement *snap_parent = GES_TIMELINE_ELEMENT_PARENT (snap_to);
 | |
|     GST_LOG_OBJECT (element, "%s (under %s) snapped with %" GES_FORMAT
 | |
|         "(under %s) from position %s%" GST_TIME_FORMAT " to %"
 | |
|         GST_TIME_FORMAT, GES_TIMELINE_ELEMENT_NAME (element),
 | |
|         parent ? parent->name : NULL, GES_ARGS (snap_to),
 | |
|         snap_parent ? snap_parent->name : NULL, negative ? "-" : "",
 | |
|         GST_TIME_ARGS (position), GST_TIME_ARGS (edge_pos));
 | |
|     snap->negative = negative;
 | |
|     snap->position = position;
 | |
|     snap->distance = distance;
 | |
|     snap->snapped = edge_pos;
 | |
|     snap->element = element;
 | |
|     snap->snapped_to = snap_to;
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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;
 | |
|   GObject *obj;
 | |
| 
 | |
|   if (!G_VALUE_HOLDS_OBJECT (value))
 | |
|     return;
 | |
| 
 | |
|   obj = g_value_get_object (value);
 | |
|   if (!GES_IS_MARKER_LIST (obj))
 | |
|     return;
 | |
| 
 | |
|   marker_list = GES_MARKER_LIST (obj);
 | |
| 
 | |
|   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)
 | |
| {
 | |
|   GESTimelineElement *element = node->data;
 | |
|   GESTrackElement *track_el, *moving;
 | |
| 
 | |
|   /* Only snap to sources */
 | |
|   /* Maybe we should allow snapping to anything that isn't an
 | |
|    * auto-transition? */
 | |
|   if (!GES_IS_SOURCE (element))
 | |
|     return FALSE;
 | |
| 
 | |
|   /* don't snap to anything we are moving */
 | |
|   if (g_hash_table_contains (data->moving, element))
 | |
|     return FALSE;
 | |
| 
 | |
|   track_el = GES_TRACK_ELEMENT (element);
 | |
|   moving = GES_TRACK_ELEMENT (data->element);
 | |
|   snap_to_edge (moving, data->position, data->negative, track_el,
 | |
|       GES_EDGE_END, data->snap);
 | |
|   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;
 | |
| }
 | |
| 
 | |
| static void
 | |
| find_snap_for_element (GESTrackElement * element, GstClockTime position,
 | |
|     gboolean negative, TreeIterationData * data)
 | |
| {
 | |
|   data->element = GES_TIMELINE_ELEMENT (element);
 | |
|   data->position = position;
 | |
|   data->negative = negative;
 | |
|   g_node_traverse (data->root, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|       (GNodeTraverseFunc) find_snap, data);
 | |
| }
 | |
| 
 | |
| /* find up to one source at the edge */
 | |
| static gboolean
 | |
| find_source_at_edge (GNode * node, TreeIterationData * data)
 | |
| {
 | |
|   GESEdge edge = data->edge;
 | |
|   GESTimelineElement *element = node->data;
 | |
|   GESTimelineElement *ancestor = data->element;
 | |
| 
 | |
|   if (!GES_IS_SOURCE (element))
 | |
|     return FALSE;
 | |
| 
 | |
|   if (ELEMENT_EDGE_VALUE (element, edge) == ELEMENT_EDGE_VALUE (ancestor, edge)) {
 | |
|     data->sources = g_list_append (data->sources, element);
 | |
|     return TRUE;
 | |
|   }
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| find_sources (GNode * node, TreeIterationData * data)
 | |
| {
 | |
|   GESTimelineElement *element = node->data;
 | |
|   if (GES_IS_SOURCE (element))
 | |
|     data->sources = g_list_append (data->sources, element);
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| /* Tries to find a new snap to the start or end edge of one of the
 | |
|  * descendant sources of @element, depending on @mode, and updates @offset
 | |
|  * by the size of the jump.
 | |
|  * Any elements in @moving are not snapped to.
 | |
|  */
 | |
| static gboolean
 | |
| timeline_tree_snap (GNode * root, GESTimelineElement * element,
 | |
|     ElementEditMode mode, GstClockTimeDiff * offset, GHashTable * moving,
 | |
|     SnappedPosition * snap)
 | |
| {
 | |
|   gboolean ret = FALSE;
 | |
|   TreeIterationData data = tree_iteration_data_init;
 | |
|   GList *tmp;
 | |
|   GNode *node;
 | |
| 
 | |
|   if (!snap)
 | |
|     return TRUE;
 | |
| 
 | |
|   /* get the sources we can snap to */
 | |
|   data.root = root;
 | |
|   data.moving = moving;
 | |
|   data.sources = NULL;
 | |
|   data.snap = snap;
 | |
|   data.element = element;
 | |
| 
 | |
|   node = find_node (root, element);
 | |
| 
 | |
|   if (!node) {
 | |
|     GST_ERROR_OBJECT (element, "Not being tracked");
 | |
|     goto done;
 | |
|   }
 | |
| 
 | |
|   switch (mode) {
 | |
|     case EDIT_MOVE:
 | |
|       /* can snap with any source below the element, if any */
 | |
|       g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|           (GNodeTraverseFunc) find_sources, &data);
 | |
|       break;
 | |
|     case EDIT_TRIM_START:
 | |
|       /* can only snap with sources at the start of the element.
 | |
|        * only need one such source since all will share the same start.
 | |
|        * if there is no source at the start edge, then snapping is not
 | |
|        * possible */
 | |
|       data.edge = GES_EDGE_START;
 | |
|       g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|           (GNodeTraverseFunc) find_source_at_edge, &data);
 | |
|       break;
 | |
|     case EDIT_TRIM_END:
 | |
|       /* can only snap with sources at the end of the element.
 | |
|        * only need one such source since all will share the same end.
 | |
|        * if there is no source at the end edge, then snapping is not
 | |
|        * possible */
 | |
|       data.edge = GES_EDGE_END;
 | |
|       g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|           (GNodeTraverseFunc) find_source_at_edge, &data);
 | |
|       break;
 | |
|     case EDIT_TRIM_INPOINT_ONLY:
 | |
|       GST_ERROR_OBJECT (element, "Trim in-point only not handled");
 | |
|       goto done;
 | |
|   }
 | |
| 
 | |
|   for (tmp = data.sources; tmp; tmp = tmp->next) {
 | |
|     GESTrackElement *source = tmp->data;
 | |
|     GstClockTime end, start;
 | |
|     gboolean negative_end, negative_start;
 | |
| 
 | |
|     /* Allow negative start/end positions in case a snap makes them valid!
 | |
|      * But we can still only snap to an existing edge in the timeline,
 | |
|      * which should be a valid time */
 | |
|     get_start_end_from_offset (GES_TIMELINE_ELEMENT (source), mode, *offset,
 | |
|         &start, &negative_start, &end, &negative_end);
 | |
| 
 | |
|     if (!GST_CLOCK_TIME_IS_VALID (start)) {
 | |
|       GST_INFO_OBJECT (element, "Cannot edit element %" GES_FORMAT
 | |
|           " with offset %" G_GINT64_FORMAT " because it would result in "
 | |
|           "an invalid start", GES_ARGS (element), *offset);
 | |
|       goto done;
 | |
|     }
 | |
| 
 | |
|     if (!GST_CLOCK_TIME_IS_VALID (end)) {
 | |
|       GST_INFO_OBJECT (element, "Cannot edit element %" GES_FORMAT
 | |
|           " with offset %" G_GINT64_FORMAT " because it would result in "
 | |
|           "an invalid end", GES_ARGS (element), *offset);
 | |
|       goto done;
 | |
|     }
 | |
| 
 | |
|     switch (mode) {
 | |
|       case EDIT_MOVE:
 | |
|         /* try snap start and end */
 | |
|         find_snap_for_element (source, end, negative_end, &data);
 | |
|         find_snap_for_element (source, start, negative_start, &data);
 | |
|         break;
 | |
|       case EDIT_TRIM_START:
 | |
|         /* only snap the start of the source */
 | |
|         find_snap_for_element (source, start, negative_start, &data);
 | |
|         break;
 | |
|       case EDIT_TRIM_END:
 | |
|         /* only snap the start of the source */
 | |
|         find_snap_for_element (source, end, negative_end, &data);
 | |
|         break;
 | |
|       case EDIT_TRIM_INPOINT_ONLY:
 | |
|         GST_ERROR_OBJECT (element, "Trim in-point only not handled");
 | |
|         goto done;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (GST_CLOCK_TIME_IS_VALID (snap->snapped)) {
 | |
|     if (snap->negative)
 | |
|       *offset -= (snap->position + snap->snapped);
 | |
|     else
 | |
|       *offset += (snap->position - snap->snapped);
 | |
|     GST_INFO_OBJECT (element, "Element %s under %s snapped with %" GES_FORMAT
 | |
|         " from %s%" GST_TIME_FORMAT " to %" GST_TIME_FORMAT,
 | |
|         GES_TIMELINE_ELEMENT_NAME (snap->element), element->name,
 | |
|         GES_ARGS (snap->snapped_to), snap->negative ? "-" : "",
 | |
|         GST_TIME_ARGS (snap->position), GST_TIME_ARGS (snap->snapped));
 | |
|   } else {
 | |
|     GST_INFO_OBJECT (element, "Nothing within snapping distance of %s",
 | |
|         element->name);
 | |
|   }
 | |
| 
 | |
|   ret = TRUE;
 | |
| 
 | |
| done:
 | |
|   g_list_free (data.sources);
 | |
| 
 | |
|   return ret;
 | |
| }
 | |
| 
 | |
| /****************************************************
 | |
|  *                 Check Overlaps                   *
 | |
|  ****************************************************/
 | |
| 
 | |
| #define _SOURCE_FORMAT "\"%s\"%s%s%s"
 | |
| #define _SOURCE_ARGS(element) \
 | |
|   element->name, element->parent ? " (parent: \"" : "", \
 | |
|   element->parent ? element->parent->name : "", \
 | |
|   element->parent ? "\")" : ""
 | |
| 
 | |
| static void
 | |
| set_full_overlap_error (GError ** error, GESTimelineElement * super,
 | |
|     GESTimelineElement * sub, GESTrack * track)
 | |
| {
 | |
|   if (error) {
 | |
|     gchar *track_name = gst_object_get_name (GST_OBJECT (track));
 | |
|     g_set_error (error, GES_ERROR, GES_ERROR_INVALID_OVERLAP_IN_TRACK,
 | |
|         "The source " _SOURCE_FORMAT " would totally overlap the "
 | |
|         "source " _SOURCE_FORMAT " in the track \"%s\"", _SOURCE_ARGS (super),
 | |
|         _SOURCE_ARGS (sub), track_name);
 | |
|     g_free (track_name);
 | |
|   }
 | |
| }
 | |
| 
 | |
| static void
 | |
| set_triple_overlap_error (GError ** error, GESTimelineElement * first,
 | |
|     GESTimelineElement * second, GESTimelineElement * third, GESTrack * track)
 | |
| {
 | |
|   if (error) {
 | |
|     gchar *track_name = gst_object_get_name (GST_OBJECT (track));
 | |
|     g_set_error (error, GES_ERROR, GES_ERROR_INVALID_OVERLAP_IN_TRACK,
 | |
|         "The sources " _SOURCE_FORMAT ", " _SOURCE_FORMAT " and "
 | |
|         _SOURCE_FORMAT " would all overlap at the same point in the "
 | |
|         "track \"%s\"", _SOURCE_ARGS (first), _SOURCE_ARGS (second),
 | |
|         _SOURCE_ARGS (third), track_name);
 | |
|     g_free (track_name);
 | |
|   }
 | |
| }
 | |
| 
 | |
| #define _ELEMENT_FORMAT \
 | |
|   "%s (under %s) [%" GST_TIME_FORMAT " - %" GST_TIME_FORMAT "] " \
 | |
|   "(layer: %" G_GUINT32_FORMAT ") (track :%" GST_PTR_FORMAT ")"
 | |
| #define _E_ARGS e->name, e->parent ? e->parent->name : NULL, \
 | |
|   GST_TIME_ARGS (start), GST_TIME_ARGS (end), layer_prio, track
 | |
| #define _CMP_ARGS cmp->name, cmp->parent ? cmp->parent->name : NULL, \
 | |
|   GST_TIME_ARGS (cmp_start), GST_TIME_ARGS (cmp_end), cmp_layer_prio, \
 | |
|   cmp_track
 | |
| 
 | |
| static gboolean
 | |
| check_overlap_with_element (GNode * node, TreeIterationData * data)
 | |
| {
 | |
|   GESTimelineElement *e = node->data, *cmp = data->element;
 | |
|   GstClockTime start, end, cmp_start, cmp_end;
 | |
|   guint32 layer_prio, cmp_layer_prio;
 | |
|   GESTrack *track, *cmp_track;
 | |
|   PositionData *pos_data;
 | |
| 
 | |
|   if (e == cmp)
 | |
|     return FALSE;
 | |
| 
 | |
|   if (!GES_IS_SOURCE (e) || !GES_IS_SOURCE (cmp))
 | |
|     return FALSE;
 | |
| 
 | |
|   /* get position of compared element */
 | |
|   pos_data = data->pos_data;
 | |
|   if (pos_data) {
 | |
|     cmp_start = pos_data->start;
 | |
|     cmp_end = pos_data->end;
 | |
|     cmp_layer_prio = pos_data->layer_priority;
 | |
|   } else {
 | |
|     cmp_start = cmp->start;
 | |
|     cmp_end = cmp_start + cmp->duration;
 | |
|     cmp_layer_prio = ges_timeline_element_get_layer_priority (cmp);
 | |
|   }
 | |
| 
 | |
|   /* get position of the node */
 | |
|   if (data->moving)
 | |
|     pos_data = g_hash_table_lookup (data->moving, e);
 | |
|   else
 | |
|     pos_data = NULL;
 | |
| 
 | |
|   if (pos_data) {
 | |
|     start = pos_data->start;
 | |
|     end = pos_data->end;
 | |
|     layer_prio = pos_data->layer_priority;
 | |
|   } else {
 | |
|     start = e->start;
 | |
|     end = start + e->duration;
 | |
|     layer_prio = ges_timeline_element_get_layer_priority (e);
 | |
|   }
 | |
| 
 | |
|   track = ges_track_element_get_track (GES_TRACK_ELEMENT (e));
 | |
|   cmp_track = ges_track_element_get_track (GES_TRACK_ELEMENT (cmp));
 | |
|   GST_LOG ("Checking overlap between " _ELEMENT_FORMAT " and "
 | |
|       _ELEMENT_FORMAT, _CMP_ARGS, _E_ARGS);
 | |
| 
 | |
|   if (track != cmp_track || track == NULL || cmp_track == NULL) {
 | |
|     GST_LOG (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " are not in the "
 | |
|         "same track", _CMP_ARGS, _E_ARGS);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   if (layer_prio != cmp_layer_prio) {
 | |
|     GST_LOG (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " are not in the "
 | |
|         "same layer", _CMP_ARGS, _E_ARGS);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   if (start >= cmp_end || cmp_start >= end) {
 | |
|     /* They do not overlap at all */
 | |
|     GST_LOG (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " do not overlap",
 | |
|         _CMP_ARGS, _E_ARGS);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   if (cmp_start <= start && cmp_end >= end) {
 | |
|     /* cmp fully overlaps e */
 | |
|     GST_INFO (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " fully overlap",
 | |
|         _CMP_ARGS, _E_ARGS);
 | |
|     set_full_overlap_error (data->error, cmp, e, track);
 | |
|     goto error;
 | |
|   }
 | |
| 
 | |
|   if (cmp_start >= start && cmp_end <= end) {
 | |
|     /* e fully overlaps cmp */
 | |
|     GST_INFO (_ELEMENT_FORMAT " and " _ELEMENT_FORMAT " fully overlap",
 | |
|         _CMP_ARGS, _E_ARGS);
 | |
|     set_full_overlap_error (data->error, e, cmp, track);
 | |
|     goto error;
 | |
|   }
 | |
| 
 | |
|   if (cmp_start < end && cmp_start > start) {
 | |
|     /* cmp_start is between the start and end of the node */
 | |
|     GST_LOG (_ELEMENT_FORMAT " is overlapped at its start by "
 | |
|         _ELEMENT_FORMAT ". Overlap ends at %" GST_TIME_FORMAT,
 | |
|         _CMP_ARGS, _E_ARGS, GST_TIME_ARGS (end));
 | |
|     if (data->overlaping_on_start) {
 | |
|       GST_INFO (_ELEMENT_FORMAT " is overlapped by %s and %s on its start",
 | |
|           _CMP_ARGS, data->overlaping_on_start->name, e->name);
 | |
|       set_triple_overlap_error (data->error, cmp, e, data->overlaping_on_start,
 | |
|           track);
 | |
|       goto error;
 | |
|     }
 | |
|     if (GST_CLOCK_TIME_IS_VALID (data->overlap_end_first_time) &&
 | |
|         end > data->overlap_end_first_time) {
 | |
|       GST_INFO (_ELEMENT_FORMAT " overlaps %s on its start and %s on its "
 | |
|           "end, but they already overlap each other", _CMP_ARGS, e->name,
 | |
|           data->overlaping_on_end->name);
 | |
|       set_triple_overlap_error (data->error, cmp, e, data->overlaping_on_end,
 | |
|           track);
 | |
|       goto error;
 | |
|     }
 | |
|     /* record the time at which the overlapped ends */
 | |
|     data->overlap_start_final_time = end;
 | |
|     data->overlaping_on_start = e;
 | |
|   }
 | |
| 
 | |
|   if (cmp_end < end && cmp_end > start) {
 | |
|     /* cmp_end is between the start and end of the node */
 | |
|     GST_LOG (_ELEMENT_FORMAT " is overlapped at its end by "
 | |
|         _ELEMENT_FORMAT ". Overlap starts at %" GST_TIME_FORMAT,
 | |
|         _CMP_ARGS, _E_ARGS, GST_TIME_ARGS (start));
 | |
| 
 | |
|     if (data->overlaping_on_end) {
 | |
|       GST_INFO (_ELEMENT_FORMAT " is overlapped by %s and %s on its end",
 | |
|           _CMP_ARGS, data->overlaping_on_end->name, e->name);
 | |
|       set_triple_overlap_error (data->error, cmp, e, data->overlaping_on_end,
 | |
|           track);
 | |
|       goto error;
 | |
|     }
 | |
|     if (GST_CLOCK_TIME_IS_VALID (data->overlap_start_final_time) &&
 | |
|         start < data->overlap_start_final_time) {
 | |
|       GST_INFO (_ELEMENT_FORMAT " overlaps %s on its end and %s on its "
 | |
|           "start, but they already overlap each other", _CMP_ARGS, e->name,
 | |
|           data->overlaping_on_start->name);
 | |
|       set_triple_overlap_error (data->error, cmp, e, data->overlaping_on_start,
 | |
|           track);
 | |
|       goto error;
 | |
|     }
 | |
|     /* record the time at which the overlapped starts */
 | |
|     data->overlap_end_first_time = start;
 | |
|     data->overlaping_on_end = e;
 | |
|   }
 | |
| 
 | |
|   return FALSE;
 | |
| 
 | |
| error:
 | |
|   data->res = FALSE;
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| /* check and find the overlaps with the element at node */
 | |
| static gboolean
 | |
| check_all_overlaps_with_element (GNode * node, TreeIterationData * data)
 | |
| {
 | |
|   GESTimelineElement *element = node->data;
 | |
|   if (GES_IS_SOURCE (element)) {
 | |
|     data->element = element;
 | |
|     data->overlaping_on_start = NULL;
 | |
|     data->overlaping_on_end = NULL;
 | |
|     data->overlap_start_final_time = GST_CLOCK_TIME_NONE;
 | |
|     data->overlap_end_first_time = GST_CLOCK_TIME_NONE;
 | |
|     if (data->moving)
 | |
|       data->pos_data = g_hash_table_lookup (data->moving, element);
 | |
|     else
 | |
|       data->pos_data = NULL;
 | |
| 
 | |
|     g_node_traverse (data->root, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|         (GNodeTraverseFunc) check_overlap_with_element, data);
 | |
| 
 | |
|     return !data->res;
 | |
|   }
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| check_moving_overlaps (GNode * node, TreeIterationData * data)
 | |
| {
 | |
|   if (g_hash_table_contains (data->moving, node->data))
 | |
|     return check_all_overlaps_with_element (node, data);
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| /* whether the elements in moving can be moved to their corresponding
 | |
|  * PositionData */
 | |
| static gboolean
 | |
| timeline_tree_can_move_elements (GNode * root, GHashTable * moving,
 | |
|     GError ** error)
 | |
| {
 | |
|   TreeIterationData data = tree_iteration_data_init;
 | |
| 
 | |
|   if (ges_timeline_get_edit_apis_disabled (root->data)) {
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   data.moving = moving;
 | |
|   data.root = root;
 | |
|   data.res = TRUE;
 | |
|   data.error = error;
 | |
|   /* sufficient to check the leaves, which is all the track elements or
 | |
|    * empty clips
 | |
|    * should also be sufficient to only check the moving elements */
 | |
|   g_node_traverse (root, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|       (GNodeTraverseFunc) check_moving_overlaps, &data);
 | |
| 
 | |
|   return data.res;
 | |
| }
 | |
| 
 | |
| /****************************************************
 | |
|  *               Setting Edit Data                  *
 | |
|  ****************************************************/
 | |
| 
 | |
| static void
 | |
| set_negative_start_error (GError ** error, GESTimelineElement * element,
 | |
|     GstClockTime neg_start)
 | |
| {
 | |
|   g_set_error (error, GES_ERROR, GES_ERROR_NEGATIVE_TIME,
 | |
|       "The element \"%s\" would have a negative start of -%"
 | |
|       GST_TIME_FORMAT, element->name, GST_TIME_ARGS (neg_start));
 | |
| }
 | |
| 
 | |
| static void
 | |
| set_negative_duration_error (GError ** error, GESTimelineElement * element,
 | |
|     GstClockTime neg_duration)
 | |
| {
 | |
|   g_set_error (error, GES_ERROR, GES_ERROR_NEGATIVE_TIME,
 | |
|       "The element \"%s\" would have a negative duration of -%"
 | |
|       GST_TIME_FORMAT, element->name, GST_TIME_ARGS (neg_duration));
 | |
| }
 | |
| 
 | |
| static void
 | |
| set_negative_inpoint_error (GError ** error, GESTimelineElement * element,
 | |
|     GstClockTime neg_inpoint)
 | |
| {
 | |
|   g_set_error (error, GES_ERROR, GES_ERROR_NEGATIVE_TIME,
 | |
|       "The element \"%s\" would have a negative in-point of -%"
 | |
|       GST_TIME_FORMAT, element->name, GST_TIME_ARGS (neg_inpoint));
 | |
| }
 | |
| 
 | |
| static void
 | |
| set_negative_layer_error (GError ** error, GESTimelineElement * element,
 | |
|     gint64 neg_layer)
 | |
| {
 | |
|   g_set_error (error, GES_ERROR, GES_ERROR_NEGATIVE_LAYER,
 | |
|       "The element \"%s\" would have a negative layer priority of -%"
 | |
|       G_GINT64_FORMAT, element->name, neg_layer);
 | |
| }
 | |
| 
 | |
| static void
 | |
| set_breaks_duration_limit_error (GError ** error, GESClip * clip,
 | |
|     GstClockTime duration, GstClockTime duration_limit)
 | |
| {
 | |
|   g_set_error (error, GES_ERROR, GES_ERROR_NOT_ENOUGH_INTERNAL_CONTENT,
 | |
|       "The clip \"%s\" would have a duration of %" GST_TIME_FORMAT
 | |
|       " that would break its duration-limit of %" GST_TIME_FORMAT,
 | |
|       GES_TIMELINE_ELEMENT_NAME (clip), GST_TIME_ARGS (duration),
 | |
|       GST_TIME_ARGS (duration_limit));
 | |
| }
 | |
| 
 | |
| static void
 | |
| set_inpoint_breaks_max_duration_error (GError ** error,
 | |
|     GESTimelineElement * element, GstClockTime inpoint,
 | |
|     GstClockTime max_duration)
 | |
| {
 | |
|   g_set_error (error, GES_ERROR, GES_ERROR_NOT_ENOUGH_INTERNAL_CONTENT,
 | |
|       "The element \"%s\" would have an in-point of %" GST_TIME_FORMAT
 | |
|       " that would break its max-duration of %" GST_TIME_FORMAT,
 | |
|       GES_TIMELINE_ELEMENT_NAME (element), GST_TIME_ARGS (inpoint),
 | |
|       GST_TIME_ARGS (max_duration));
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| set_layer_priority (GESTimelineElement * element, EditData * data,
 | |
|     GError ** error)
 | |
| {
 | |
|   gint64 layer_offset = data->layer_offset;
 | |
|   guint32 layer_prio = ges_timeline_element_get_layer_priority (element);
 | |
| 
 | |
|   if (!layer_offset)
 | |
|     return TRUE;
 | |
| 
 | |
|   if (layer_prio == GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY) {
 | |
|     GST_INFO_OBJECT (element, "Cannot shift %s to a new layer because it "
 | |
|         "has no layer priority", element->name);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   if (layer_offset > (gint64) layer_prio) {
 | |
|     GST_INFO_OBJECT (element, "%s would have a negative layer priority (%"
 | |
|         G_GUINT32_FORMAT " - %" G_GINT64_FORMAT ")", element->name,
 | |
|         layer_prio, layer_offset);
 | |
|     set_negative_layer_error (error, element,
 | |
|         layer_offset - (gint64) layer_prio);
 | |
|     return FALSE;
 | |
|   }
 | |
|   if ((layer_prio - (gint64) layer_offset) >= G_MAXUINT32) {
 | |
|     GST_ERROR_OBJECT (element, "%s would have an overflowing layer priority",
 | |
|         element->name);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   data->layer_priority = (guint32) (layer_prio - (gint64) layer_offset);
 | |
| 
 | |
|   if (ges_timeline_layer_priority_in_gap (element->timeline,
 | |
|           data->layer_priority)) {
 | |
|     GST_ERROR_OBJECT (element, "Edit layer %" G_GUINT32_FORMAT " would "
 | |
|         "be within a gap in the timeline layers", data->layer_priority);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   GST_INFO_OBJECT (element, "%s will move to layer %" G_GUINT32_FORMAT,
 | |
|       element->name, data->layer_priority);
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| #define _CHECK_END(element, start, duration) \
 | |
|   if (!GST_CLOCK_TIME_IS_VALID (_clock_time_plus (start, duration))) { \
 | |
|     GST_INFO_OBJECT (element, "Cannot edit %s because it would result in " \
 | |
|         "an invalid end", element->name); \
 | |
|     return FALSE; \
 | |
|   }
 | |
| 
 | |
| static gboolean
 | |
| set_edit_move_values (GESTimelineElement * element, EditData * data,
 | |
|     GError ** error)
 | |
| {
 | |
|   gboolean negative = FALSE;
 | |
|   GstClockTime new_start =
 | |
|       _clock_time_minus_diff (element->start, data->offset, &negative);
 | |
|   if (negative || !GST_CLOCK_TIME_IS_VALID (new_start)) {
 | |
|     GST_INFO_OBJECT (element, "Cannot move %" GES_FORMAT " with offset %"
 | |
|         G_GINT64_FORMAT " because it would result in an invalid start",
 | |
|         GES_ARGS (element), data->offset);
 | |
|     if (negative)
 | |
|       set_negative_start_error (error, element, new_start);
 | |
|     return FALSE;
 | |
|   }
 | |
|   _CHECK_END (element, new_start, element->duration);
 | |
|   data->start = new_start;
 | |
| 
 | |
|   if (GES_IS_GROUP (element))
 | |
|     return TRUE;
 | |
| 
 | |
|   GST_INFO_OBJECT (element, "%s will move by setting start to %"
 | |
|       GST_TIME_FORMAT, element->name, GST_TIME_ARGS (data->start));
 | |
| 
 | |
|   return set_layer_priority (element, data, error);
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| set_edit_trim_start_clip_inpoints (GESClip * clip, EditData * clip_data,
 | |
|     GHashTable * edit_table, GError ** error)
 | |
| {
 | |
|   gboolean ret = FALSE;
 | |
|   GList *tmp;
 | |
|   GstClockTime duration_limit;
 | |
|   GstClockTime clip_inpoint;
 | |
|   GstClockTime new_start = clip_data->start;
 | |
|   gboolean no_core = FALSE;
 | |
|   GHashTable *child_inpoints;
 | |
| 
 | |
|   child_inpoints = g_hash_table_new_full (NULL, NULL, gst_object_unref, g_free);
 | |
| 
 | |
|   clip_inpoint = ges_clip_get_core_internal_time_from_timeline_time (clip,
 | |
|       new_start, &no_core, error);
 | |
| 
 | |
|   if (no_core) {
 | |
|     GST_INFO_OBJECT (clip, "Clip %" GES_FORMAT " has no active core "
 | |
|         "children with an internal source. Not setting in-point during "
 | |
|         "trim to start", GES_ARGS (clip));
 | |
|     clip_inpoint = GES_TIMELINE_ELEMENT_INPOINT (clip);
 | |
|   } else if (!GST_CLOCK_TIME_IS_VALID (clip_inpoint)) {
 | |
|     GST_INFO_OBJECT (clip, "Cannot trim start of %" GES_FORMAT
 | |
|         " with offset %" G_GINT64_FORMAT " because it would result in an "
 | |
|         "invalid in-point for its core children", GES_ARGS (clip),
 | |
|         clip_data->offset);
 | |
|     goto done;
 | |
|   } else {
 | |
|     GST_INFO_OBJECT (clip, "Clip %" GES_FORMAT " will have its in-point "
 | |
|         " set to %" GST_TIME_FORMAT " because its start is being trimmed "
 | |
|         "to %" GST_TIME_FORMAT, GES_ARGS (clip),
 | |
|         GST_TIME_ARGS (clip_inpoint), GST_TIME_ARGS (new_start));
 | |
|     clip_data->inpoint = clip_inpoint;
 | |
|   }
 | |
| 
 | |
|   /* need to set in-point of active non-core children to keep their
 | |
|    * internal content at the same timeline position */
 | |
|   for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next) {
 | |
|     GESTimelineElement *child = tmp->data;
 | |
|     GESTrackElement *el = tmp->data;
 | |
|     GstClockTime new_inpoint = child->inpoint;
 | |
|     GstClockTime *inpoint_p;
 | |
| 
 | |
|     if (ges_track_element_has_internal_source (el)) {
 | |
|       if (ges_track_element_is_core (el)) {
 | |
|         new_inpoint = clip_inpoint;
 | |
|       } else if (ges_track_element_is_active (el)) {
 | |
|         EditData *data;
 | |
| 
 | |
|         if (g_hash_table_contains (edit_table, child)) {
 | |
|           GST_ERROR_OBJECT (child, "Already set to be edited");
 | |
|           goto done;
 | |
|         }
 | |
| 
 | |
|         new_inpoint = ges_clip_get_internal_time_from_timeline_time (clip, el,
 | |
|             new_start, error);
 | |
| 
 | |
|         if (!GST_CLOCK_TIME_IS_VALID (new_inpoint)) {
 | |
|           GST_INFO_OBJECT (clip, "Cannot trim start of %" GES_FORMAT
 | |
|               " to %" GST_TIME_FORMAT " because it would result in an "
 | |
|               "invalid in-point for the non-core child %" GES_FORMAT,
 | |
|               GES_ARGS (clip), GST_TIME_ARGS (new_start), GES_ARGS (child));
 | |
|           goto done;
 | |
|         }
 | |
| 
 | |
|         GST_INFO_OBJECT (child, "Setting track element %s to trim "
 | |
|             "in-point to %" GST_TIME_FORMAT " since the parent clip %"
 | |
|             GES_FORMAT " is being trimmed to start %" GST_TIME_FORMAT,
 | |
|             child->name, GST_TIME_ARGS (new_inpoint), GES_ARGS (clip),
 | |
|             GST_TIME_ARGS (new_start));
 | |
| 
 | |
|         data = new_edit_data (EDIT_TRIM_INPOINT_ONLY, 0, 0);
 | |
|         data->inpoint = new_inpoint;
 | |
|         g_hash_table_insert (edit_table, child, data);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (GES_CLOCK_TIME_IS_LESS (child->maxduration, new_inpoint)) {
 | |
|       GST_INFO_OBJECT (clip, "Cannot trim start of %" GES_FORMAT
 | |
|           " to %" GST_TIME_FORMAT " because it would result in an "
 | |
|           "in-point of %" GST_TIME_FORMAT " for the child %" GES_FORMAT
 | |
|           ", which breaks its max-duration", GES_ARGS (clip),
 | |
|           GST_TIME_ARGS (new_start), GST_TIME_ARGS (new_inpoint),
 | |
|           GES_ARGS (child));
 | |
| 
 | |
|       set_inpoint_breaks_max_duration_error (error, child, new_inpoint,
 | |
|           child->maxduration);
 | |
|       goto done;
 | |
|     }
 | |
| 
 | |
|     inpoint_p = g_new (GstClockTime, 1);
 | |
|     *inpoint_p = new_inpoint;
 | |
|     g_hash_table_insert (child_inpoints, gst_object_ref (child), inpoint_p);
 | |
|   }
 | |
| 
 | |
|   duration_limit =
 | |
|       ges_clip_duration_limit_with_new_children_inpoints (clip, child_inpoints);
 | |
| 
 | |
|   if (GES_CLOCK_TIME_IS_LESS (duration_limit, clip_data->duration)) {
 | |
|     GST_INFO_OBJECT (clip, "Cannot trim start of %" GES_FORMAT
 | |
|         " to %" GST_TIME_FORMAT " because it would result in a "
 | |
|         "duration of %" GST_TIME_FORMAT " that breaks its new "
 | |
|         "duration-limit of %" GST_TIME_FORMAT, GES_ARGS (clip),
 | |
|         GST_TIME_ARGS (new_start), GST_TIME_ARGS (clip_data->duration),
 | |
|         GST_TIME_ARGS (duration_limit));
 | |
| 
 | |
|     set_breaks_duration_limit_error (error, clip, clip_data->duration,
 | |
|         duration_limit);
 | |
|     goto done;
 | |
|   }
 | |
| 
 | |
|   ret = TRUE;
 | |
| 
 | |
| done:
 | |
|   g_hash_table_unref (child_inpoints);
 | |
| 
 | |
|   return ret;
 | |
| }
 | |
| 
 | |
| /* trim the start of a clip or a track element */
 | |
| static gboolean
 | |
| set_edit_trim_start_values (GESTimelineElement * element, EditData * data,
 | |
|     GHashTable * edit_table, GError ** error)
 | |
| {
 | |
|   gboolean negative = FALSE;
 | |
|   GstClockTime new_duration;
 | |
|   GstClockTime new_start =
 | |
|       _clock_time_minus_diff (element->start, data->offset, &negative);
 | |
| 
 | |
|   if (negative || !GST_CLOCK_TIME_IS_VALID (new_start)) {
 | |
|     GST_INFO_OBJECT (element, "Cannot trim start of %" GES_FORMAT
 | |
|         " with offset %" G_GINT64_FORMAT " because it would result in an "
 | |
|         "invalid start", GES_ARGS (element), data->offset);
 | |
|     if (negative)
 | |
|       set_negative_start_error (error, element, new_start);
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   new_duration =
 | |
|       _clock_time_minus_diff (element->duration, -data->offset, &negative);
 | |
| 
 | |
|   if (negative || !GST_CLOCK_TIME_IS_VALID (new_duration)) {
 | |
|     GST_INFO_OBJECT (element, "Cannot trim start of %" GES_FORMAT
 | |
|         " with offset %" G_GINT64_FORMAT " because it would result in an "
 | |
|         "invalid duration", GES_ARGS (element), data->offset);
 | |
|     if (negative)
 | |
|       set_negative_duration_error (error, element, new_duration);
 | |
|     return FALSE;
 | |
|   }
 | |
|   _CHECK_END (element, new_start, new_duration);
 | |
| 
 | |
|   data->start = new_start;
 | |
|   data->duration = new_duration;
 | |
| 
 | |
|   if (GES_IS_GROUP (element))
 | |
|     return TRUE;
 | |
| 
 | |
|   if (GES_IS_CLIP (element)) {
 | |
|     if (!set_edit_trim_start_clip_inpoints (GES_CLIP (element), data,
 | |
|             edit_table, error))
 | |
|       return FALSE;
 | |
|   } else if (GES_IS_TRACK_ELEMENT (element)
 | |
|       && ges_track_element_has_internal_source (GES_TRACK_ELEMENT (element))) {
 | |
|     GstClockTime new_inpoint =
 | |
|         _clock_time_minus_diff (element->inpoint, data->offset, &negative);
 | |
| 
 | |
|     if (negative || !GST_CLOCK_TIME_IS_VALID (new_inpoint)) {
 | |
|       GST_INFO_OBJECT (element, "Cannot trim start of %" GES_FORMAT
 | |
|           " with offset %" G_GINT64_FORMAT " because it would result in "
 | |
|           "an invalid in-point", GES_ARGS (element), data->offset);
 | |
|       if (negative)
 | |
|         set_negative_inpoint_error (error, element, new_inpoint);
 | |
|       return FALSE;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   GST_INFO_OBJECT (element, "%s will trim start by setting start to %"
 | |
|       GST_TIME_FORMAT ", in-point to %" GST_TIME_FORMAT " and duration "
 | |
|       "to %" GST_TIME_FORMAT, element->name, GST_TIME_ARGS (data->start),
 | |
|       GST_TIME_ARGS (data->inpoint), GST_TIME_ARGS (data->duration));
 | |
| 
 | |
|   return set_layer_priority (element, data, error);
 | |
| }
 | |
| 
 | |
| /* trim the end of a clip or a track element */
 | |
| static gboolean
 | |
| set_edit_trim_end_values (GESTimelineElement * element, EditData * data,
 | |
|     GError ** error)
 | |
| {
 | |
|   gboolean negative = FALSE;
 | |
|   GstClockTime new_duration =
 | |
|       _clock_time_minus_diff (element->duration, data->offset, &negative);
 | |
|   if (negative || !GST_CLOCK_TIME_IS_VALID (new_duration)) {
 | |
|     GST_INFO_OBJECT (element, "Cannot trim end of %" GES_FORMAT
 | |
|         " with offset %" G_GINT64_FORMAT " because it would result in an "
 | |
|         "invalid duration", GES_ARGS (element), data->offset);
 | |
|     if (negative)
 | |
|       set_negative_duration_error (error, element, new_duration);
 | |
|     return FALSE;
 | |
|   }
 | |
|   _CHECK_END (element, element->start, new_duration);
 | |
| 
 | |
|   if (GES_IS_CLIP (element)) {
 | |
|     GESClip *clip = GES_CLIP (element);
 | |
|     GstClockTime limit = ges_clip_get_duration_limit (clip);
 | |
| 
 | |
|     if (GES_CLOCK_TIME_IS_LESS (limit, new_duration)) {
 | |
|       GST_INFO_OBJECT (element, "Cannot trim end of %" GES_FORMAT
 | |
|           " with offset %" G_GINT64_FORMAT " because the duration would "
 | |
|           "exceed the clip's duration-limit %" G_GINT64_FORMAT,
 | |
|           GES_ARGS (element), data->offset, limit);
 | |
| 
 | |
|       set_breaks_duration_limit_error (error, clip, new_duration, limit);
 | |
|       return FALSE;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   data->duration = new_duration;
 | |
| 
 | |
|   if (GES_IS_GROUP (element))
 | |
|     return TRUE;
 | |
| 
 | |
|   GST_INFO_OBJECT (element, "%s will trim end by setting duration to %"
 | |
|       GST_TIME_FORMAT, element->name, GST_TIME_ARGS (data->duration));
 | |
| 
 | |
|   return set_layer_priority (element, data, error);
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| set_edit_values (GESTimelineElement * element, EditData * data,
 | |
|     GHashTable * edit_table, GError ** error)
 | |
| {
 | |
|   switch (data->mode) {
 | |
|     case EDIT_MOVE:
 | |
|       return set_edit_move_values (element, data, error);
 | |
|     case EDIT_TRIM_START:
 | |
|       return set_edit_trim_start_values (element, data, edit_table, error);
 | |
|     case EDIT_TRIM_END:
 | |
|       return set_edit_trim_end_values (element, data, error);
 | |
|     case EDIT_TRIM_INPOINT_ONLY:
 | |
|       GST_ERROR_OBJECT (element, "Trim in-point only not handled");
 | |
|       return FALSE;
 | |
|   }
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| add_clips_to_list (GNode * node, GList ** list)
 | |
| {
 | |
|   GESTimelineElement *element = node->data;
 | |
|   GESTimelineElement *clip = NULL;
 | |
| 
 | |
|   if (GES_IS_CLIP (element))
 | |
|     clip = element;
 | |
|   else if (GES_IS_CLIP (element->parent))
 | |
|     clip = element->parent;
 | |
| 
 | |
|   if (clip && !g_list_find (*list, clip))
 | |
|     *list = g_list_append (*list, clip);
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| replace_group_with_clip_edits (GNode * root, GESTimelineElement * group,
 | |
|     GHashTable * edit_table, GError ** err)
 | |
| {
 | |
|   gboolean ret = TRUE;
 | |
|   GList *tmp, *clips = NULL;
 | |
|   GNode *node = find_node (root, group);
 | |
|   GstClockTime new_end, new_start;
 | |
|   ElementEditMode mode;
 | |
|   gint64 layer_offset;
 | |
| 
 | |
|   if (!node) {
 | |
|     GST_ERROR_OBJECT (group, "Not being tracked");
 | |
|     goto error;
 | |
|   }
 | |
| 
 | |
|   /* new context for the lifespan of group_data */
 | |
|   {
 | |
|     EditData *group_edit = g_hash_table_lookup (edit_table, group);
 | |
| 
 | |
|     if (!group_edit) {
 | |
|       GST_ERROR_OBJECT (group, "Edit data for group was missing");
 | |
|       goto error;
 | |
|     }
 | |
| 
 | |
|     group_edit->start = group->start;
 | |
|     group_edit->duration = group->duration;
 | |
| 
 | |
|     /* should only set the start and duration fields, table should not be
 | |
|      * needed, so we pass NULL */
 | |
|     if (!set_edit_values (group, group_edit, NULL, err))
 | |
|       goto error;
 | |
| 
 | |
|     new_start = group_edit->start;
 | |
|     new_end = _clock_time_plus (group_edit->start, group_edit->duration);
 | |
| 
 | |
|     if (!GST_CLOCK_TIME_IS_VALID (new_start)
 | |
|         || !GST_CLOCK_TIME_IS_VALID (new_end)) {
 | |
|       GST_ERROR_OBJECT (group, "Edit data gave an invalid start or end");
 | |
|       goto error;
 | |
|     }
 | |
| 
 | |
|     layer_offset = group_edit->layer_offset;
 | |
|     mode = group_edit->mode;
 | |
| 
 | |
|     /* can traverse leaves to find all the clips since they are at _most_
 | |
|      * one step above the track elements */
 | |
|     g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|         (GNodeTraverseFunc) add_clips_to_list, &clips);
 | |
| 
 | |
|     if (!clips) {
 | |
|       GST_INFO_OBJECT (group, "Contains no clips, so cannot be edited");
 | |
|       goto error;
 | |
|     }
 | |
| 
 | |
|     if (!g_hash_table_remove (edit_table, group)) {
 | |
|       GST_ERROR_OBJECT (group, "Could not replace the group in the edit list");
 | |
|       goto error;
 | |
|     }
 | |
|     /* removing the group from the table frees group_edit */
 | |
|   }
 | |
| 
 | |
|   for (tmp = clips; tmp; tmp = tmp->next) {
 | |
|     GESTimelineElement *clip = tmp->data;
 | |
|     gboolean edit = FALSE;
 | |
|     GstClockTimeDiff offset = G_MAXINT64;
 | |
|     ElementEditMode clip_mode = mode;
 | |
| 
 | |
|     /* if at the edge of the group and being trimmed forward or backward */
 | |
|     if (mode == EDIT_MOVE) {
 | |
|       /* same offset as the group */
 | |
|       edit = TRUE;
 | |
|       offset = group->start - new_start;
 | |
| 
 | |
|       GST_INFO_OBJECT (clip, "Setting clip %s to moving with offset %"
 | |
|           G_GINT64_FORMAT " since an ancestor group %" GES_FORMAT
 | |
|           " is moving to %" GST_TIME_FORMAT, clip->name, offset,
 | |
|           GES_ARGS (group), GST_TIME_ARGS (new_start));
 | |
| 
 | |
|     } else if ((mode == EDIT_TRIM_START)
 | |
|         && (clip->start <= new_start || clip->start == group->start)) {
 | |
|       /* trim to same start */
 | |
|       edit = TRUE;
 | |
|       offset = clip->start - new_start;
 | |
| 
 | |
|       GST_INFO_OBJECT (clip, "Setting clip %s to trim start with offset %"
 | |
|           G_GINT64_FORMAT " since an ancestor group %" GES_FORMAT " is "
 | |
|           "being trimmed to start %" GST_TIME_FORMAT, clip->name, offset,
 | |
|           GES_ARGS (group), GST_TIME_ARGS (new_start));
 | |
| 
 | |
|     } else if (mode == EDIT_TRIM_END
 | |
|         && (_END (clip) >= new_end || _END (clip) == _END (group))) {
 | |
|       /* trim to same end */
 | |
|       edit = TRUE;
 | |
|       offset = _END (clip) - new_end;
 | |
| 
 | |
|       GST_INFO_OBJECT (clip, "Setting clip %s to trim end with offset %"
 | |
|           G_GINT64_FORMAT " since an ancestor group %" GES_FORMAT " is "
 | |
|           "being trimmed to end %" GST_TIME_FORMAT, clip->name, offset,
 | |
|           GES_ARGS (group), GST_TIME_ARGS (new_end));
 | |
| 
 | |
|     } else if (layer_offset) {
 | |
|       /* still need to move layer */
 | |
|       edit = TRUE;
 | |
|       clip_mode = EDIT_MOVE;
 | |
|       offset = 0;
 | |
|     }
 | |
|     if (edit) {
 | |
|       EditData *clip_data;
 | |
| 
 | |
|       if (layer_offset)
 | |
|         GST_INFO_OBJECT (clip, "Setting clip %s to move to new layer with "
 | |
|             "offset %" G_GINT64_FORMAT " since an ancestor group %"
 | |
|             GES_FORMAT " is being moved with the same offset", clip->name,
 | |
|             layer_offset, GES_ARGS (group));
 | |
| 
 | |
|       if (g_hash_table_contains (edit_table, clip)) {
 | |
|         GST_ERROR_OBJECT (clip, "Already set to be edited");
 | |
|         goto error;
 | |
|       }
 | |
|       clip_data = new_edit_data (clip_mode, offset, layer_offset);
 | |
|       g_hash_table_insert (edit_table, clip, clip_data);
 | |
|       if (!set_edit_values (clip, clip_data, edit_table, err))
 | |
|         goto error;
 | |
|     }
 | |
|   }
 | |
| 
 | |
| done:
 | |
|   g_list_free (clips);
 | |
|   return ret;
 | |
| 
 | |
| error:
 | |
|   ret = FALSE;
 | |
|   goto done;
 | |
| }
 | |
| 
 | |
| /* set the edit values for the entries in @edits
 | |
|  * any groups in @edits will be replaced by their clip children */
 | |
| static gboolean
 | |
| timeline_tree_set_element_edit_values (GNode * root, GHashTable * edits,
 | |
|     GError ** err)
 | |
| {
 | |
|   gboolean ret = TRUE;
 | |
|   GESTimelineElement *element;
 | |
|   EditData *edit_data;
 | |
|   /* content of edit table may change when group edits are replaced by
 | |
|    * clip edits and clip edits introduce edits for non-core children */
 | |
|   GList *tmp, *elements = g_hash_table_get_keys (edits);
 | |
| 
 | |
|   for (tmp = elements; tmp; tmp = tmp->next) {
 | |
|     gboolean res;
 | |
|     element = tmp->data;
 | |
|     edit_data = g_hash_table_lookup (edits, element);
 | |
|     if (!edit_data) {
 | |
|       GST_ERROR_OBJECT (element, "No edit data for the element");
 | |
|       goto error;
 | |
|     }
 | |
|     if (GES_IS_GROUP (element))
 | |
|       res = replace_group_with_clip_edits (root, element, edits, err);
 | |
|     else
 | |
|       res = set_edit_values (element, edit_data, edits, err);
 | |
|     if (!res)
 | |
|       goto error;
 | |
|   }
 | |
| 
 | |
| done:
 | |
|   g_list_free (elements);
 | |
| 
 | |
|   return ret;
 | |
| 
 | |
| error:
 | |
|   ret = FALSE;
 | |
|   goto done;
 | |
| }
 | |
| 
 | |
| /* set the moving PositionData by using their parent clips.
 | |
|  * @edit_table should already have had its values set, and any group edits
 | |
|  * replaced by clip edits. */
 | |
| static void
 | |
| set_moving_positions_from_edits (GHashTable * moving, GHashTable * edit_table)
 | |
| {
 | |
|   GHashTableIter iter;
 | |
|   gpointer key, value;
 | |
| 
 | |
|   g_hash_table_iter_init (&iter, moving);
 | |
|   while (g_hash_table_iter_next (&iter, &key, &value)) {
 | |
|     GESTimelineElement *element = key;
 | |
|     PositionData *pos = value;
 | |
|     GESTimelineElement *parent;
 | |
|     EditData *edit;
 | |
| 
 | |
|     /* a track element will end up with the same start and end as its clip */
 | |
|     /* if no parent, act as own parent */
 | |
|     parent = element->parent ? element->parent : element;
 | |
|     edit = g_hash_table_lookup (edit_table, parent);
 | |
| 
 | |
|     if (edit && GST_CLOCK_TIME_IS_VALID (edit->start))
 | |
|       pos->start = edit->start;
 | |
|     else
 | |
|       pos->start = element->start;
 | |
| 
 | |
|     if (edit && GST_CLOCK_TIME_IS_VALID (edit->duration))
 | |
|       pos->end = pos->start + edit->duration;
 | |
|     else
 | |
|       pos->end = pos->start + element->duration;
 | |
| 
 | |
|     if (edit && edit->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY)
 | |
|       pos->layer_priority = edit->layer_priority;
 | |
|     else
 | |
|       pos->layer_priority = ges_timeline_element_get_layer_priority (element);
 | |
|   }
 | |
| }
 | |
| 
 | |
| static void
 | |
| give_edits_same_offset (GHashTable * edits, GstClockTimeDiff offset,
 | |
|     gint64 layer_offset)
 | |
| {
 | |
|   GHashTableIter iter;
 | |
|   gpointer value;
 | |
| 
 | |
|   g_hash_table_iter_init (&iter, edits);
 | |
|   while (g_hash_table_iter_next (&iter, NULL, &value)) {
 | |
|     EditData *edit_data = value;
 | |
|     edit_data->offset = offset;
 | |
|     edit_data->layer_offset = layer_offset;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /****************************************************
 | |
|  *         Initialise Edit Data and Moving          *
 | |
|  ****************************************************/
 | |
| 
 | |
| static gboolean
 | |
| add_track_elements_to_moving (GNode * node, GHashTable * track_elements)
 | |
| {
 | |
|   GESTimelineElement *element = node->data;
 | |
|   if (GES_IS_TRACK_ELEMENT (element)) {
 | |
|     GST_LOG_OBJECT (element, "%s set as moving", element->name);
 | |
|     g_hash_table_insert (track_elements, element, g_new0 (PositionData, 1));
 | |
|   }
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| /* add all the track elements found under the elements in @edits to @moving,
 | |
|  * but does not set their position data */
 | |
| static gboolean
 | |
| timeline_tree_add_edited_to_moving (GNode * root, GHashTable * edits,
 | |
|     GHashTable * moving)
 | |
| {
 | |
|   GHashTableIter iter;
 | |
|   gpointer key;
 | |
| 
 | |
|   g_hash_table_iter_init (&iter, edits);
 | |
|   while (g_hash_table_iter_next (&iter, &key, NULL)) {
 | |
|     GESTimelineElement *element = key;
 | |
|     GNode *node = find_node (root, element);
 | |
|     if (!node) {
 | |
|       GST_ERROR_OBJECT (element, "Not being tracked");
 | |
|       return FALSE;
 | |
|     }
 | |
|     g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|         (GNodeTraverseFunc) add_track_elements_to_moving, moving);
 | |
|   }
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| /* check we can handle the top and all of its children */
 | |
| static gboolean
 | |
| check_types (GESTimelineElement * element, gboolean is_top)
 | |
| {
 | |
|   if (!GES_IS_CLIP (element) && !GES_IS_GROUP (element)
 | |
|       && !GES_IS_TRACK_ELEMENT (element)) {
 | |
|     GST_ERROR_OBJECT (element, "Cannot handle a GESTimelineElement of the "
 | |
|         "type %s", G_OBJECT_TYPE_NAME (element));
 | |
|     return FALSE;
 | |
|   }
 | |
|   if (!is_top && element->parent) {
 | |
|     if ((GES_IS_CLIP (element) && !GES_IS_GROUP (element->parent))
 | |
|         || (GES_IS_GROUP (element) && !GES_IS_GROUP (element->parent))
 | |
|         || (GES_IS_TRACK_ELEMENT (element) && !GES_IS_CLIP (element->parent))) {
 | |
|       GST_ERROR_OBJECT (element, "A parent of type %s is not handled",
 | |
|           G_OBJECT_TYPE_NAME (element->parent));
 | |
|       return FALSE;
 | |
|     }
 | |
|   }
 | |
|   if (GES_IS_CONTAINER (element)) {
 | |
|     GList *tmp;
 | |
|     for (tmp = GES_CONTAINER_CHILDREN (element); tmp; tmp = tmp->next) {
 | |
|       if (!check_types (tmp->data, FALSE))
 | |
|         return FALSE;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| /* @edits: The table to add the edit to
 | |
|  * @element: The element to edit
 | |
|  * @mode: The mode for editing @element
 | |
|  *
 | |
|  * Adds an edit for @element it to the table with its EditData only set
 | |
|  * with @mode.
 | |
|  *
 | |
|  * The offsets for the edit will have to be set later.
 | |
|  */
 | |
| static gboolean
 | |
| add_element_edit (GHashTable * edits, GESTimelineElement * element,
 | |
|     ElementEditMode mode)
 | |
| {
 | |
|   if (!check_types (element, TRUE))
 | |
|     return FALSE;
 | |
| 
 | |
|   if (g_hash_table_contains (edits, element)) {
 | |
|     GST_ERROR_OBJECT (element, "Already set to be edited");
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   switch (mode) {
 | |
|     case EDIT_MOVE:
 | |
|       GST_LOG_OBJECT (element, "%s set to move", element->name);
 | |
|       break;
 | |
|     case EDIT_TRIM_START:
 | |
|       GST_LOG_OBJECT (element, "%s set to trim start", element->name);
 | |
|       break;
 | |
|     case EDIT_TRIM_END:
 | |
|       GST_LOG_OBJECT (element, "%s set to trim end", element->name);
 | |
|       break;
 | |
|     case EDIT_TRIM_INPOINT_ONLY:
 | |
|       GST_ERROR_OBJECT (element, "%s set to trim in-point only", element->name);
 | |
|       return FALSE;
 | |
|   }
 | |
| 
 | |
|   g_hash_table_insert (edits, element, new_edit_data (mode, 0, 0));
 | |
| 
 | |
|   return TRUE;
 | |
| }
 | |
| 
 | |
| /********************************************
 | |
|  *   Check against current configuration    *
 | |
|  ********************************************/
 | |
| 
 | |
| /* can move with no snapping or change in parent! */
 | |
| gboolean
 | |
| timeline_tree_can_move_element (GNode * root,
 | |
|     GESTimelineElement * element, guint32 priority, GstClockTime start,
 | |
|     GstClockTime duration, GError ** error)
 | |
| {
 | |
|   gboolean ret = FALSE;
 | |
|   guint32 layer_prio = ges_timeline_element_get_layer_priority (element);
 | |
|   GstClockTime distance, new_end;
 | |
|   GHashTable *move_edits, *trim_edits, *moving;
 | |
|   GHashTableIter iter;
 | |
|   gpointer key, value;
 | |
| 
 | |
|   if (ges_timeline_get_edit_apis_disabled (root->data)) {
 | |
|     return TRUE;
 | |
|   }
 | |
| 
 | |
|   if (layer_prio == GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY
 | |
|       && priority != layer_prio) {
 | |
|     GST_INFO_OBJECT (element, "Cannot move to a layer when no layer "
 | |
|         "priority to begin with");
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   distance = _abs_clock_time_distance (start, element->start);
 | |
|   if ((GstClockTimeDiff) distance >= G_MAXINT64) {
 | |
|     GST_WARNING_OBJECT (element, "Move in start from %" GST_TIME_FORMAT
 | |
|         " to %" GST_TIME_FORMAT " is too large to perform",
 | |
|         GST_TIME_ARGS (element->start), GST_TIME_ARGS (start));
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   distance = _abs_clock_time_distance (duration, element->duration);
 | |
|   if ((GstClockTimeDiff) distance >= G_MAXINT64) {
 | |
|     GST_WARNING_OBJECT (element, "Move in duration from %" GST_TIME_FORMAT
 | |
|         " to %" GST_TIME_FORMAT " is too large to perform",
 | |
|         GST_TIME_ARGS (element->duration), GST_TIME_ARGS (duration));
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   new_end = _clock_time_plus (start, duration);
 | |
|   if (!GST_CLOCK_TIME_IS_VALID (new_end)) {
 | |
|     GST_WARNING_OBJECT (element, "Move in start and duration to %"
 | |
|         GST_TIME_FORMAT " and %" GST_TIME_FORMAT " would produce an "
 | |
|         "invalid end", GST_TIME_ARGS (start), GST_TIME_ARGS (duration));
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   /* treat as an EDIT_MOVE to the new priority, except on the element
 | |
|    * rather than the toplevel, followed by an EDIT_TRIM_END */
 | |
|   move_edits = new_edit_table ();
 | |
|   trim_edits = new_edit_table ();
 | |
|   moving = new_position_table ();
 | |
| 
 | |
|   if (!add_element_edit (move_edits, element, EDIT_MOVE))
 | |
|     goto done;
 | |
|   /* moving should remain the same */
 | |
|   if (!add_element_edit (trim_edits, element, EDIT_TRIM_END))
 | |
|     goto done;
 | |
| 
 | |
|   if (!timeline_tree_add_edited_to_moving (root, move_edits, moving)
 | |
|       || !timeline_tree_add_edited_to_moving (root, trim_edits, moving))
 | |
|     goto done;
 | |
| 
 | |
|   /* no snapping */
 | |
|   give_edits_same_offset (move_edits, element->start - start,
 | |
|       (gint64) layer_prio - (gint64) priority);
 | |
|   give_edits_same_offset (trim_edits, element->duration - duration, 0);
 | |
| 
 | |
|   /* assume both edits can be performed if each could occur individually */
 | |
|   /* should not effect duration or in-point */
 | |
|   if (!timeline_tree_set_element_edit_values (root, move_edits, error))
 | |
|     goto done;
 | |
|   /* should not effect start or in-point or layer */
 | |
|   if (!timeline_tree_set_element_edit_values (root, trim_edits, error))
 | |
|     goto done;
 | |
| 
 | |
|   /* merge the two edits into moving positions */
 | |
|   g_hash_table_iter_init (&iter, moving);
 | |
|   while (g_hash_table_iter_next (&iter, &key, &value)) {
 | |
|     GESTimelineElement *el = key;
 | |
|     PositionData *pos_data = value;
 | |
|     EditData *move = NULL;
 | |
|     EditData *trim = NULL;
 | |
| 
 | |
|     if (el->parent) {
 | |
|       move = g_hash_table_lookup (move_edits, el->parent);
 | |
|       trim = g_hash_table_lookup (trim_edits, el->parent);
 | |
|     }
 | |
| 
 | |
|     if (!move)
 | |
|       move = g_hash_table_lookup (move_edits, el);
 | |
|     if (!trim)
 | |
|       trim = g_hash_table_lookup (trim_edits, el);
 | |
| 
 | |
|     /* should always have move with a valid start */
 | |
|     if (!move || !GST_CLOCK_TIME_IS_VALID (move->start)) {
 | |
|       GST_ERROR_OBJECT (el, "Element set to moving but neither it nor its "
 | |
|           "parent are being edited");
 | |
|       goto done;
 | |
|     }
 | |
|     /* may not have trim if element is a group and the child is away
 | |
|      * from the edit position, but if we do it should have a valid duration */
 | |
|     if (trim && !GST_CLOCK_TIME_IS_VALID (trim->duration)) {
 | |
|       GST_ERROR_OBJECT (el, "Element set to trim end but neither it nor its "
 | |
|           "parent is being trimmed");
 | |
|       goto done;
 | |
|     }
 | |
| 
 | |
|     pos_data->start = move->start;
 | |
| 
 | |
|     if (move->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY)
 | |
|       pos_data->layer_priority = move->layer_priority;
 | |
|     else
 | |
|       pos_data->layer_priority = ges_timeline_element_get_layer_priority (el);
 | |
| 
 | |
|     if (trim)
 | |
|       pos_data->end = pos_data->start + trim->duration;
 | |
|     else
 | |
|       pos_data->end = pos_data->start + el->duration;
 | |
|   }
 | |
| 
 | |
|   /* check overlaps */
 | |
|   if (!timeline_tree_can_move_elements (root, moving, error))
 | |
|     goto done;
 | |
| 
 | |
|   ret = TRUE;
 | |
| 
 | |
| done:
 | |
|   g_hash_table_unref (trim_edits);
 | |
|   g_hash_table_unref (move_edits);
 | |
|   g_hash_table_unref (moving);
 | |
| 
 | |
|   return ret;
 | |
| }
 | |
| 
 | |
| /********************************************
 | |
|  *         Perform Element Edit             *
 | |
|  ********************************************/
 | |
| 
 | |
| static gboolean
 | |
| perform_element_edit (GESTimelineElement * element, EditData * edit)
 | |
| {
 | |
|   gboolean ret = FALSE;
 | |
|   guint32 layer_prio = ges_timeline_element_get_layer_priority (element);
 | |
| 
 | |
|   switch (edit->mode) {
 | |
|     case EDIT_MOVE:
 | |
|       GST_INFO_OBJECT (element, "Moving %s from %" GST_TIME_FORMAT " to %"
 | |
|           GST_TIME_FORMAT, element->name, GST_TIME_ARGS (element->start),
 | |
|           GST_TIME_ARGS (edit->start));
 | |
|       break;
 | |
|     case EDIT_TRIM_START:
 | |
|       GST_INFO_OBJECT (element, "Trimming %s start from %" GST_TIME_FORMAT
 | |
|           " to %" GST_TIME_FORMAT, element->name,
 | |
|           GST_TIME_ARGS (element->start), GST_TIME_ARGS (edit->start));
 | |
|       break;
 | |
|     case EDIT_TRIM_END:
 | |
|       GST_INFO_OBJECT (element, "Trimming %s end from %" GST_TIME_FORMAT
 | |
|           " to %" GST_TIME_FORMAT, element->name,
 | |
|           GST_TIME_ARGS (_END (element)),
 | |
|           GST_TIME_ARGS (element->start + edit->duration));
 | |
|       break;
 | |
|     case EDIT_TRIM_INPOINT_ONLY:
 | |
|       GST_INFO_OBJECT (element, "Trimming %s in-point from %"
 | |
|           GST_TIME_FORMAT " to %" GST_TIME_FORMAT, element->name,
 | |
|           GST_TIME_ARGS (element->inpoint), GST_TIME_ARGS (edit->inpoint));
 | |
|       break;
 | |
|   }
 | |
| 
 | |
|   if (!GES_IS_CLIP (element) && !GES_IS_TRACK_ELEMENT (element)) {
 | |
|     GST_ERROR_OBJECT (element, "Cannot perform edit on group");
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   if (!GES_IS_CLIP (element)
 | |
|       && edit->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY) {
 | |
|     GST_ERROR_OBJECT (element, "Cannot move an element that is not a "
 | |
|         "clip to a new layer");
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   GES_TIMELINE_ELEMENT_SET_BEING_EDITED (element);
 | |
|   if (GST_CLOCK_TIME_IS_VALID (edit->start)) {
 | |
|     if (!ges_timeline_element_set_start (element, edit->start)) {
 | |
|       GST_ERROR_OBJECT (element, "Failed to set the start");
 | |
|       goto done;
 | |
|     }
 | |
|   }
 | |
|   if (GST_CLOCK_TIME_IS_VALID (edit->inpoint)) {
 | |
|     if (!ges_timeline_element_set_inpoint (element, edit->inpoint)) {
 | |
|       GST_ERROR_OBJECT (element, "Failed to set the in-point");
 | |
|       goto done;
 | |
|     }
 | |
|   }
 | |
|   if (GST_CLOCK_TIME_IS_VALID (edit->duration)) {
 | |
|     if (!ges_timeline_element_set_duration (element, edit->duration)) {
 | |
|       GST_ERROR_OBJECT (element, "Failed to set the duration");
 | |
|       goto done;
 | |
|     }
 | |
|   }
 | |
|   if (edit->layer_priority != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY) {
 | |
|     GESTimeline *timeline = GES_TIMELINE_ELEMENT_TIMELINE (element);
 | |
|     GESLayer *layer = ges_timeline_get_layer (timeline, edit->layer_priority);
 | |
| 
 | |
|     GST_INFO_OBJECT (element, "Moving %s from layer %" G_GUINT32_FORMAT
 | |
|         " to layer %" G_GUINT32_FORMAT, element->name, layer_prio,
 | |
|         edit->layer_priority);
 | |
| 
 | |
|     if (layer == NULL) {
 | |
|       /* make sure we won't loop forever */
 | |
|       if (ges_timeline_layer_priority_in_gap (timeline, edit->layer_priority)) {
 | |
|         GST_ERROR_OBJECT (element, "Requested layer %" G_GUINT32_FORMAT
 | |
|             " is within a gap in the timeline layers", edit->layer_priority);
 | |
|         goto done;
 | |
|       }
 | |
| 
 | |
|       do {
 | |
|         layer = ges_timeline_append_layer (timeline);
 | |
|       } while (ges_layer_get_priority (layer) < edit->layer_priority);
 | |
|     } else {
 | |
|       gst_object_unref (layer);
 | |
|     }
 | |
| 
 | |
|     if (!ges_clip_move_to_layer (GES_CLIP (element), layer)) {
 | |
|       GST_ERROR_OBJECT (element, "Failed to move layers");
 | |
|       goto done;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   ret = TRUE;
 | |
| 
 | |
| done:
 | |
|   GES_TIMELINE_ELEMENT_UNSET_BEING_EDITED (element);
 | |
| 
 | |
|   return ret;
 | |
| }
 | |
| 
 | |
| /* perform all the element edits found in @edits.
 | |
|  * These should only be clips of track elements. */
 | |
| static gboolean
 | |
| timeline_tree_perform_edits (GNode * root, GHashTable * edits)
 | |
| {
 | |
|   gboolean no_errors = TRUE;
 | |
|   GHashTableIter iter;
 | |
|   gpointer key, value;
 | |
| 
 | |
|   /* freeze the auto-transitions whilst we edit */
 | |
|   ges_timeline_freeze_auto_transitions (root->data, TRUE);
 | |
| 
 | |
|   g_hash_table_iter_init (&iter, edits);
 | |
|   while (g_hash_table_iter_next (&iter, &key, &value)) {
 | |
|     if (GES_IS_TRACK_ELEMENT (key))
 | |
|       ges_track_element_freeze_control_sources (GES_TRACK_ELEMENT (key), TRUE);
 | |
|   }
 | |
| 
 | |
|   g_hash_table_iter_init (&iter, edits);
 | |
|   while (g_hash_table_iter_next (&iter, &key, &value)) {
 | |
|     GESTimelineElement *element = key;
 | |
|     EditData *edit_data = value;
 | |
|     if (!perform_element_edit (element, edit_data))
 | |
|       no_errors = FALSE;
 | |
|   }
 | |
| 
 | |
|   g_hash_table_iter_init (&iter, edits);
 | |
|   while (g_hash_table_iter_next (&iter, &key, &value)) {
 | |
|     if (GES_IS_TRACK_ELEMENT (key))
 | |
|       ges_track_element_freeze_control_sources (GES_TRACK_ELEMENT (key), FALSE);
 | |
|   }
 | |
| 
 | |
|   /* allow the transitions to update if they can */
 | |
|   ges_timeline_freeze_auto_transitions (root->data, FALSE);
 | |
| 
 | |
|   timeline_tree_create_transitions (root, ges_timeline_find_auto_transition);
 | |
|   timeline_update_duration (root->data);
 | |
| 
 | |
|   return no_errors;
 | |
| }
 | |
| 
 | |
| #define _REPLACE_TRACK_ELEMENT_WITH_PARENT(element) \
 | |
|   element = (GES_IS_TRACK_ELEMENT (element) && element->parent) ? element->parent : element
 | |
| 
 | |
| /********************************************
 | |
|  *                 Ripple                   *
 | |
|  ********************************************/
 | |
| 
 | |
| gboolean
 | |
| timeline_tree_ripple (GNode * root, GESTimelineElement * element,
 | |
|     gint64 layer_priority_offset, GstClockTimeDiff offset, GESEdge edge,
 | |
|     GstClockTime snapping_distance, GError ** error)
 | |
| {
 | |
|   gboolean res = TRUE;
 | |
|   GNode *node;
 | |
|   GESTimelineElement *ripple_toplevel;
 | |
|   GstClockTime ripple_time;
 | |
|   GHashTable *edits = new_edit_table ();
 | |
|   GHashTable *moving = new_position_table ();
 | |
|   ElementEditMode mode;
 | |
|   SnappedPosition *snap = new_snapped_position (snapping_distance);
 | |
| 
 | |
|   _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
 | |
| 
 | |
|   ripple_toplevel = ges_timeline_element_peak_toplevel (element);
 | |
| 
 | |
|   /* if EDGE_END:
 | |
|    *   TRIM_END the element, and MOVE all toplevels whose start is after
 | |
|    *   the current end of the element by the same amount
 | |
|    * otherwise:
 | |
|    *   MOVE the topevel of the element, and all other toplevel elements
 | |
|    *   whose start is after the current start of the element */
 | |
| 
 | |
|   switch (edge) {
 | |
|     case GES_EDGE_END:
 | |
|       GST_INFO_OBJECT (element, "Rippling end with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       mode = EDIT_TRIM_END;
 | |
|       break;
 | |
|     case GES_EDGE_START:
 | |
|       GST_INFO_OBJECT (element, "Rippling start with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       mode = EDIT_MOVE;
 | |
|       break;
 | |
|     case GES_EDGE_NONE:
 | |
|       GST_INFO_OBJECT (element, "Rippling with toplevel with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       element = ripple_toplevel;
 | |
|       mode = EDIT_MOVE;
 | |
|       break;
 | |
|     default:
 | |
|       GST_WARNING_OBJECT (element, "Edge not supported");
 | |
|       goto done;
 | |
|   }
 | |
| 
 | |
|   ripple_time = ELEMENT_EDGE_VALUE (element, edge);
 | |
| 
 | |
|   /* add edits */
 | |
|   if (!add_element_edit (edits, element, mode))
 | |
|     goto error;
 | |
| 
 | |
|   for (node = root->children; node; node = node->next) {
 | |
|     GESTimelineElement *toplevel = node->data;
 | |
|     if (toplevel == ripple_toplevel)
 | |
|       continue;
 | |
| 
 | |
|     if (toplevel->start >= ripple_time) {
 | |
|       if (!add_element_edit (edits, toplevel, EDIT_MOVE))
 | |
|         goto error;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (!timeline_tree_add_edited_to_moving (root, edits, moving))
 | |
|     goto error;
 | |
| 
 | |
|   /* snap */
 | |
|   if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
 | |
|     goto error;
 | |
| 
 | |
|   /* check and set edits using snapped values */
 | |
|   give_edits_same_offset (edits, offset, layer_priority_offset);
 | |
|   if (!timeline_tree_set_element_edit_values (root, edits, error))
 | |
|     goto error;
 | |
| 
 | |
|   /* check overlaps */
 | |
|   set_moving_positions_from_edits (moving, edits);
 | |
|   if (!timeline_tree_can_move_elements (root, moving, error))
 | |
|     goto error;
 | |
| 
 | |
|   /* emit snapping now. Edits should only fail if a programming error
 | |
|    * occured */
 | |
|   if (snap)
 | |
|     ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
 | |
|         snap->snapped);
 | |
| 
 | |
|   res = timeline_tree_perform_edits (root, edits);
 | |
| 
 | |
| done:
 | |
|   g_hash_table_unref (edits);
 | |
|   g_hash_table_unref (moving);
 | |
|   g_free (snap);
 | |
|   return res;
 | |
| 
 | |
| error:
 | |
|   res = FALSE;
 | |
|   goto done;
 | |
| }
 | |
| 
 | |
| /********************************************
 | |
|  *                  Trim                    *
 | |
|  ********************************************/
 | |
| 
 | |
| gboolean
 | |
| timeline_tree_trim (GNode * root, GESTimelineElement * element,
 | |
|     gint64 layer_priority_offset, GstClockTimeDiff offset, GESEdge edge,
 | |
|     GstClockTime snapping_distance, GError ** error)
 | |
| {
 | |
|   gboolean res = TRUE;
 | |
|   GHashTable *edits = new_edit_table ();
 | |
|   GHashTable *moving = new_position_table ();
 | |
|   ElementEditMode mode;
 | |
|   SnappedPosition *snap = new_snapped_position (snapping_distance);
 | |
| 
 | |
|   _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
 | |
| 
 | |
|   /* TODO: 2.0 remove this warning and simply fail if no edge is specified */
 | |
|   if (edge == GES_EDGE_NONE) {
 | |
|     g_warning ("No edge specified for trimming. Defaulting to GES_EDGE_START");
 | |
|     edge = GES_EDGE_START;
 | |
|   }
 | |
| 
 | |
|   switch (edge) {
 | |
|     case GES_EDGE_END:
 | |
|       GST_INFO_OBJECT (element, "Trimming end with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       mode = EDIT_TRIM_END;
 | |
|       break;
 | |
|     case GES_EDGE_START:
 | |
|       GST_INFO_OBJECT (element, "Trimming start with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       mode = EDIT_TRIM_START;
 | |
|       break;
 | |
|     default:
 | |
|       GST_WARNING_OBJECT (element, "Edge not supported");
 | |
|       goto done;
 | |
|   }
 | |
| 
 | |
|   /* add edits */
 | |
|   if (!add_element_edit (edits, element, mode))
 | |
|     goto error;
 | |
| 
 | |
|   if (!timeline_tree_add_edited_to_moving (root, edits, moving))
 | |
|     goto error;
 | |
| 
 | |
|   /* snap */
 | |
|   if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
 | |
|     goto error;
 | |
| 
 | |
|   /* check and set edits using snapped values */
 | |
|   give_edits_same_offset (edits, offset, layer_priority_offset);
 | |
|   if (!timeline_tree_set_element_edit_values (root, edits, error))
 | |
|     goto error;
 | |
| 
 | |
|   /* check overlaps */
 | |
|   set_moving_positions_from_edits (moving, edits);
 | |
|   if (!timeline_tree_can_move_elements (root, moving, error)) {
 | |
|     goto error;
 | |
|   }
 | |
| 
 | |
|   /* emit snapping now. Edits should only fail if a programming error
 | |
|    * occured */
 | |
|   if (snap)
 | |
|     ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
 | |
|         snap->snapped);
 | |
| 
 | |
|   res = timeline_tree_perform_edits (root, edits);
 | |
| 
 | |
| done:
 | |
|   g_hash_table_unref (edits);
 | |
|   g_hash_table_unref (moving);
 | |
|   g_free (snap);
 | |
|   return res;
 | |
| 
 | |
| error:
 | |
|   res = FALSE;
 | |
|   goto done;
 | |
| }
 | |
| 
 | |
| /********************************************
 | |
|  *                  Move                    *
 | |
|  ********************************************/
 | |
| 
 | |
| gboolean
 | |
| timeline_tree_move (GNode * root, GESTimelineElement * element,
 | |
|     gint64 layer_priority_offset, GstClockTimeDiff offset, GESEdge edge,
 | |
|     GstClockTime snapping_distance, GError ** error)
 | |
| {
 | |
|   gboolean res = TRUE;
 | |
|   GHashTable *edits = new_edit_table ();
 | |
|   GHashTable *moving = new_position_table ();
 | |
|   ElementEditMode mode;
 | |
|   SnappedPosition *snap = new_snapped_position (snapping_distance);
 | |
| 
 | |
|   _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
 | |
| 
 | |
|   switch (edge) {
 | |
|     case GES_EDGE_END:
 | |
|       GST_INFO_OBJECT (element, "Moving end with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       mode = EDIT_TRIM_END;
 | |
|       break;
 | |
|     case GES_EDGE_START:
 | |
|       GST_INFO_OBJECT (element, "Moving start with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       mode = EDIT_MOVE;
 | |
|       break;
 | |
|     case GES_EDGE_NONE:
 | |
|       GST_INFO_OBJECT (element, "Moving with toplevel with offset %"
 | |
|           G_GINT64_FORMAT " and layer offset %" G_GINT64_FORMAT, offset,
 | |
|           layer_priority_offset);
 | |
|       element = ges_timeline_element_peak_toplevel (element);
 | |
|       mode = EDIT_MOVE;
 | |
|       break;
 | |
|     default:
 | |
|       GST_WARNING_OBJECT (element, "Edge not supported");
 | |
|       goto done;
 | |
|   }
 | |
| 
 | |
|   /* add edits */
 | |
|   if (!add_element_edit (edits, element, mode))
 | |
|     goto error;
 | |
| 
 | |
|   if (!timeline_tree_add_edited_to_moving (root, edits, moving))
 | |
|     goto error;
 | |
| 
 | |
|   /* snap */
 | |
|   if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
 | |
|     goto error;
 | |
| 
 | |
|   /* check and set edits using snapped values */
 | |
|   give_edits_same_offset (edits, offset, layer_priority_offset);
 | |
|   if (!timeline_tree_set_element_edit_values (root, edits, error))
 | |
|     goto error;
 | |
| 
 | |
|   /* check overlaps */
 | |
|   set_moving_positions_from_edits (moving, edits);
 | |
|   if (!timeline_tree_can_move_elements (root, moving, error)) {
 | |
|     goto error;
 | |
|   }
 | |
| 
 | |
|   /* emit snapping now. Edits should only fail if a programming error
 | |
|    * occured */
 | |
|   if (snap)
 | |
|     ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
 | |
|         snap->snapped);
 | |
| 
 | |
|   res = timeline_tree_perform_edits (root, edits);
 | |
| 
 | |
| done:
 | |
|   g_hash_table_unref (edits);
 | |
|   g_hash_table_unref (moving);
 | |
|   g_free (snap);
 | |
|   return res;
 | |
| 
 | |
| error:
 | |
|   res = FALSE;
 | |
|   goto done;
 | |
| }
 | |
| 
 | |
| /********************************************
 | |
|  *                  Roll                    *
 | |
|  ********************************************/
 | |
| 
 | |
| static gboolean
 | |
| is_descendant (GESTimelineElement * element, GESTimelineElement * ancestor)
 | |
| {
 | |
|   GESTimelineElement *parent = element;
 | |
|   while ((parent = parent->parent)) {
 | |
|     if (parent == ancestor)
 | |
|       return TRUE;
 | |
|   }
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| find_neighbour (GNode * node, TreeIterationData * data)
 | |
| {
 | |
|   GList *tmp;
 | |
|   gboolean in_same_track = FALSE;
 | |
|   GESTimelineElement *edge_element, *element = node->data;
 | |
| 
 | |
|   if (!GES_IS_SOURCE (element))
 | |
|     return FALSE;
 | |
| 
 | |
|   /* if the element is controlled by the trimmed element (a group or a
 | |
|    * clip) it is not a neighbour */
 | |
|   if (is_descendant (element, data->element))
 | |
|     return FALSE;
 | |
| 
 | |
|   /* test if we share a track with one of the sources at the edge */
 | |
|   for (tmp = data->sources; tmp; tmp = tmp->next) {
 | |
|     if (ges_track_element_get_track (GES_TRACK_ELEMENT (element)) ==
 | |
|         ges_track_element_get_track (tmp->data)) {
 | |
|       in_same_track = TRUE;
 | |
|       break;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (!in_same_track)
 | |
|     return FALSE;
 | |
| 
 | |
|   /* get the most toplevel element whose edge touches the position */
 | |
|   edge_element = NULL;
 | |
|   while (element && ELEMENT_EDGE_VALUE (element, data->edge) == data->position) {
 | |
|     edge_element = element;
 | |
|     element = element->parent;
 | |
|   }
 | |
| 
 | |
|   if (edge_element && !g_list_find (data->neighbours, edge_element))
 | |
|     data->neighbours = g_list_prepend (data->neighbours, edge_element);
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| find_sources_at_position (GNode * node, TreeIterationData * data)
 | |
| {
 | |
|   GESTimelineElement *element = node->data;
 | |
| 
 | |
|   if (!GES_IS_SOURCE (element))
 | |
|     return FALSE;
 | |
| 
 | |
|   if (ELEMENT_EDGE_VALUE (element, data->edge) == data->position)
 | |
|     data->sources = g_list_append (data->sources, element);
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| gboolean
 | |
| timeline_tree_roll (GNode * root, GESTimelineElement * element,
 | |
|     GstClockTimeDiff offset, GESEdge edge, GstClockTime snapping_distance,
 | |
|     GError ** error)
 | |
| {
 | |
|   gboolean res = TRUE;
 | |
|   GList *tmp;
 | |
|   GNode *node;
 | |
|   TreeIterationData data = tree_iteration_data_init;
 | |
|   GHashTable *edits = new_edit_table ();
 | |
|   GHashTable *moving = new_position_table ();
 | |
|   ElementEditMode mode;
 | |
|   SnappedPosition *snap = new_snapped_position (snapping_distance);
 | |
| 
 | |
|   _REPLACE_TRACK_ELEMENT_WITH_PARENT (element);
 | |
| 
 | |
|   /* if EDGE_END:
 | |
|    *   TRIM_END the element, and TRIM_START the neighbouring clips to the
 | |
|    *   end edge
 | |
|    * otherwise:
 | |
|    *   TRIM_START the element, and TRIM_END the neighbouring clips to the
 | |
|    *   start edge */
 | |
| 
 | |
|   switch (edge) {
 | |
|     case GES_EDGE_END:
 | |
|       GST_INFO_OBJECT (element, "Rolling end with offset %"
 | |
|           G_GINT64_FORMAT, offset);
 | |
|       mode = EDIT_TRIM_END;
 | |
|       break;
 | |
|     case GES_EDGE_START:
 | |
|       GST_INFO_OBJECT (element, "Rolling start with offset %"
 | |
|           G_GINT64_FORMAT, offset);
 | |
|       mode = EDIT_TRIM_START;
 | |
|       break;
 | |
|     case GES_EDGE_NONE:
 | |
|       GST_WARNING_OBJECT (element, "Need to select an edge when rolling.");
 | |
|       goto done;
 | |
|     default:
 | |
|       GST_WARNING_OBJECT (element, "Edge not supported");
 | |
|       goto done;
 | |
|   }
 | |
| 
 | |
|   /* add edits */
 | |
|   if (!add_element_edit (edits, element, mode))
 | |
|     goto error;
 | |
| 
 | |
|   /* first, find all the sources at the edge */
 | |
|   node = find_node (root, element);
 | |
|   if (!node) {
 | |
|     GST_ERROR_OBJECT (element, "Not being tracked");
 | |
|     goto error;
 | |
|   }
 | |
| 
 | |
|   data.element = element;
 | |
|   data.edge = (edge == GES_EDGE_END) ? GES_EDGE_END : GES_EDGE_START;
 | |
|   data.position = ELEMENT_EDGE_VALUE (element, data.edge);
 | |
|   data.sources = NULL;
 | |
| 
 | |
|   g_node_traverse (node, G_IN_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|       (GNodeTraverseFunc) find_sources_at_position, &data);
 | |
| 
 | |
|   /* find elements that whose opposite edge touches the edge of the
 | |
|    * element and shares a track with one of the found sources */
 | |
|   data.edge = (edge == GES_EDGE_END) ? GES_EDGE_START : GES_EDGE_END;
 | |
|   data.neighbours = NULL;
 | |
| 
 | |
|   g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1,
 | |
|       (GNodeTraverseFunc) find_neighbour, &data);
 | |
| 
 | |
|   for (tmp = data.neighbours; tmp; tmp = tmp->next) {
 | |
|     GESTimelineElement *clip = tmp->data;
 | |
|     ElementEditMode opposite =
 | |
|         (mode == EDIT_TRIM_END) ? EDIT_TRIM_START : EDIT_TRIM_END;
 | |
|     if (!add_element_edit (edits, clip, opposite))
 | |
|       goto error;
 | |
|   }
 | |
| 
 | |
|   if (!timeline_tree_add_edited_to_moving (root, edits, moving))
 | |
|     goto error;
 | |
| 
 | |
|   /* snap */
 | |
|   if (!timeline_tree_snap (root, element, mode, &offset, moving, snap))
 | |
|     goto error;
 | |
| 
 | |
|   /* check and set edits using snapped values */
 | |
|   give_edits_same_offset (edits, offset, 0);
 | |
|   if (!timeline_tree_set_element_edit_values (root, edits, error))
 | |
|     goto error;
 | |
| 
 | |
|   /* check overlaps */
 | |
|   set_moving_positions_from_edits (moving, edits);
 | |
|   if (!timeline_tree_can_move_elements (root, moving, error)) {
 | |
|     goto error;
 | |
|   }
 | |
| 
 | |
|   /* emit snapping now. Edits should only fail if a programming error
 | |
|    * occured */
 | |
|   if (snap)
 | |
|     ges_timeline_emit_snapping (root->data, snap->element, snap->snapped_to,
 | |
|         snap->snapped);
 | |
| 
 | |
|   res = timeline_tree_perform_edits (root, edits);
 | |
| 
 | |
| done:
 | |
|   g_hash_table_unref (edits);
 | |
|   g_hash_table_unref (moving);
 | |
|   g_list_free (data.neighbours);
 | |
|   g_list_free (data.sources);
 | |
|   g_free (snap);
 | |
|   return res;
 | |
| 
 | |
| error:
 | |
|   res = FALSE;
 | |
|   goto done;
 | |
| }
 | |
| 
 | |
| static void
 | |
| create_transition_if_needed (GESTimeline * timeline, GESTrackElement * prev,
 | |
|     GESTrackElement * next, GESTreeGetAutoTransitionFunc get_auto_transition)
 | |
| {
 | |
|   GstClockTime duration = _END (prev) - _START (next);
 | |
|   GESAutoTransition *trans =
 | |
|       get_auto_transition (timeline, prev, next, duration);
 | |
| 
 | |
|   if (!trans) {
 | |
|     GESLayer *layer = ges_timeline_get_layer (timeline,
 | |
|         GES_TIMELINE_ELEMENT_LAYER_PRIORITY (prev));
 | |
|     gst_object_unref (layer);
 | |
| 
 | |
|     GST_INFO ("Creating transition [%" G_GINT64_FORMAT " - %" G_GINT64_FORMAT
 | |
|         "]", _START (next), duration);
 | |
|     ges_timeline_create_transition (timeline, prev, next, NULL, layer,
 | |
|         _START (next), duration);
 | |
|   } else {
 | |
|     GST_INFO ("Already have transition %" GST_PTR_FORMAT " between %" GES_FORMAT
 | |
|         " and %" GES_FORMAT, trans, GES_ARGS (prev), GES_ARGS (next));
 | |
|   }
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| create_transitions (GNode * node,
 | |
|     GESTreeGetAutoTransitionFunc get_auto_transition)
 | |
| {
 | |
|   TreeIterationData data = tree_iteration_data_init;
 | |
|   GESTimeline *timeline;
 | |
|   GESLayer *layer;
 | |
| 
 | |
|   if (!GES_IS_SOURCE (node->data))
 | |
|     return FALSE;
 | |
| 
 | |
|   timeline = GES_TIMELINE_ELEMENT_TIMELINE (node->data);
 | |
| 
 | |
|   if (!timeline) {
 | |
|     GST_INFO ("%" GES_FORMAT " not in timeline yet", GES_ARGS (node->data));
 | |
| 
 | |
|     return FALSE;
 | |
|   }
 | |
| 
 | |
|   layer =
 | |
|       ges_timeline_get_layer (timeline,
 | |
|       GES_TIMELINE_ELEMENT_LAYER_PRIORITY (node->data));
 | |
|   gst_object_unref (layer);
 | |
| 
 | |
|   if (!ges_layer_get_auto_transition (layer))
 | |
|     return FALSE;
 | |
| 
 | |
|   GST_LOG_OBJECT (node->data, "Checking for overlaps");
 | |
|   data.root = g_node_get_root (node);
 | |
|   check_all_overlaps_with_element (node, &data);
 | |
| 
 | |
|   if (data.overlaping_on_start)
 | |
|     create_transition_if_needed (timeline,
 | |
|         GES_TRACK_ELEMENT (data.overlaping_on_start), node->data,
 | |
|         get_auto_transition);
 | |
| 
 | |
|   if (data.overlaping_on_end)
 | |
|     create_transition_if_needed (timeline, node->data,
 | |
|         GES_TRACK_ELEMENT (data.overlaping_on_end), get_auto_transition);
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_create_transitions_for_track_element (GNode * root,
 | |
|     GESTrackElement * element, GESTreeGetAutoTransitionFunc get_auto_transition)
 | |
| {
 | |
|   GNode *node = find_node (root, element);
 | |
|   g_assert (node);
 | |
| 
 | |
|   create_transitions (node, get_auto_transition);
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_create_transitions (GNode * root,
 | |
|     GESTreeGetAutoTransitionFunc get_auto_transition)
 | |
| {
 | |
|   g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAFS, -1,
 | |
|       (GNodeTraverseFunc) create_transitions, get_auto_transition);
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| compute_duration (GNode * node, GstClockTime * duration)
 | |
| {
 | |
|   *duration = MAX (_END (node->data), *duration);
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| GstClockTime
 | |
| timeline_tree_get_duration (GNode * root)
 | |
| {
 | |
|   GstClockTime duration = 0;
 | |
| 
 | |
|   if (root->children)
 | |
|     g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAFS, -1,
 | |
|         (GNodeTraverseFunc) compute_duration, &duration);
 | |
| 
 | |
|   return duration;
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| reset_layer_activness (GNode * node, GESLayer * layer)
 | |
| {
 | |
|   GESTrack *track;
 | |
| 
 | |
| 
 | |
|   if (!GES_IS_TRACK_ELEMENT (node->data))
 | |
|     return FALSE;
 | |
| 
 | |
|   track = ges_track_element_get_track (node->data);
 | |
|   if (!track || (ges_timeline_element_get_layer_priority (node->data) !=
 | |
|           ges_layer_get_priority (layer)))
 | |
|     return FALSE;
 | |
| 
 | |
|   ges_track_element_set_layer_active (node->data,
 | |
|       ges_layer_get_active_for_track (layer, track));
 | |
| 
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_reset_layer_active (GNode * root, GESLayer * layer)
 | |
| {
 | |
|   g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAFS, -1,
 | |
|       (GNodeTraverseFunc) reset_layer_activness, layer);
 | |
| }
 | |
| 
 | |
| static gboolean
 | |
| set_is_smart_rendering (GNode * node, gboolean * is_rendering_smartly)
 | |
| {
 | |
|   if (!GES_IS_SOURCE (node->data))
 | |
|     return FALSE;
 | |
| 
 | |
|   ges_source_set_rendering_smartly (GES_SOURCE (node->data),
 | |
|       *is_rendering_smartly);
 | |
|   return FALSE;
 | |
| }
 | |
| 
 | |
| void
 | |
| timeline_tree_set_smart_rendering (GNode * root, gboolean rendering_smartly)
 | |
| {
 | |
|   g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAFS, -1,
 | |
|       (GNodeTraverseFunc) set_is_smart_rendering, &rendering_smartly);
 | |
| }
 |