2576 lines
78 KiB
C
2576 lines
78 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 ()
|
|
{
|
|
return g_hash_table_new_full (NULL, NULL, NULL, g_free);
|
|
}
|
|
|
|
static GHashTable *
|
|
new_position_table ()
|
|
{
|
|
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;
|
|
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 (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 (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);
|
|
}
|