gstreamer/subprojects/gst-editing-services/ges/ges-timeline-tree.c
2024-04-19 08:03:48 +00:00

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 ()
{
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", &timestamp, NULL);
timestamp =
ges_clip_get_timeline_time_from_internal_time (parent_clip, marker_parent,
timestamp, NULL);
snap_to_marker (moving, data->position, data->negative, timestamp,
marker_parent, data->snap);
g_object_unref (marker);
}
static gboolean
find_snap (GNode * node, TreeIterationData * data)
{
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);
}