mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-21 07:46:38 +00:00
822e0a7b09
In the GESTimeline, TrackElement addition to a clip might get cancelled (and thus the element gets removed), we need to make sure users do not get wrong signals. Also document the fact that user should connect to container::child-added with g_signal_connect_after.
3604 lines
109 KiB
C
3604 lines
109 KiB
C
/* GStreamer Editing Services
|
|
* Copyright (C) 2009 Edward Hervey <edward.hervey@collabora.co.uk>
|
|
* 2009 Nokia Corporation
|
|
* 2012 Thibault Saunier <tsaunier@gnome.org>
|
|
* 2012 Collabora Ltd.
|
|
* Author: Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/**
|
|
* SECTION:gestimeline
|
|
* @short_description: Multimedia timeline
|
|
*
|
|
* #GESTimeline is the central object for any multimedia timeline.
|
|
*
|
|
* Contains a list of #GESLayer which users should use to arrange the
|
|
* various clips through time.
|
|
*
|
|
* The output type is determined by the #GESTrack that are set on
|
|
* the #GESTimeline.
|
|
*
|
|
* To save/load a timeline, you can use the ges_timeline_load_from_uri() and
|
|
* ges_timeline_save_to_uri() methods to use the default format. If you wish
|
|
*
|
|
* Note that any change you make in the timeline will not actually be taken
|
|
* into account until you call the #ges_timeline_commit method.
|
|
*/
|
|
|
|
#include "ges-internal.h"
|
|
#include "ges-project.h"
|
|
#include "ges-container.h"
|
|
#include "ges-timeline.h"
|
|
#include "ges-track.h"
|
|
#include "ges-layer.h"
|
|
#include "ges-auto-transition.h"
|
|
#include "ges.h"
|
|
|
|
typedef struct _MoveContext MoveContext;
|
|
|
|
static GPtrArray *select_tracks_for_object_default (GESTimeline * timeline,
|
|
GESClip * clip, GESTrackElement * tr_obj, gpointer user_data);
|
|
static inline void init_movecontext (MoveContext * mv_ctx, gboolean first_init);
|
|
static void ges_extractable_interface_init (GESExtractableInterface * iface);
|
|
static void ges_meta_container_interface_init
|
|
(GESMetaContainerInterface * iface);
|
|
|
|
G_DEFINE_TYPE_WITH_CODE (GESTimeline, ges_timeline, GST_TYPE_BIN,
|
|
G_IMPLEMENT_INTERFACE (GES_TYPE_EXTRACTABLE, ges_extractable_interface_init)
|
|
G_IMPLEMENT_INTERFACE (GES_TYPE_META_CONTAINER,
|
|
ges_meta_container_interface_init));
|
|
|
|
|
|
GST_DEBUG_CATEGORY_STATIC (ges_timeline_debug);
|
|
#undef GST_CAT_DEFAULT
|
|
#define GST_CAT_DEFAULT ges_timeline_debug
|
|
|
|
/* lock to protect dynamic callbacks, like pad-added */
|
|
#define DYN_LOCK(timeline) (&GES_TIMELINE (timeline)->priv->dyn_mutex)
|
|
#define LOCK_DYN(timeline) G_STMT_START { \
|
|
GST_INFO_OBJECT (timeline, "Getting dynamic lock from %p", \
|
|
g_thread_self()); \
|
|
g_rec_mutex_lock (DYN_LOCK (timeline)); \
|
|
GST_INFO_OBJECT (timeline, "Got Dynamic lock from %p", \
|
|
g_thread_self()); \
|
|
} G_STMT_END
|
|
|
|
#define UNLOCK_DYN(timeline) G_STMT_START { \
|
|
GST_INFO_OBJECT (timeline, "Unlocking dynamic lock from %p", \
|
|
g_thread_self()); \
|
|
g_rec_mutex_unlock (DYN_LOCK (timeline)); \
|
|
GST_INFO_OBJECT (timeline, "Unlocked Dynamic lock from %p", \
|
|
g_thread_self()); \
|
|
} G_STMT_END
|
|
|
|
typedef struct TrackObjIters
|
|
{
|
|
GSequenceIter *iter_start;
|
|
GSequenceIter *iter_end;
|
|
GSequenceIter *iter_obj;
|
|
GSequenceIter *iter_by_layer;
|
|
|
|
GESLayer *layer;
|
|
GESTrackElement *trackelement;
|
|
} TrackObjIters;
|
|
|
|
static void
|
|
_destroy_obj_iters (TrackObjIters * iters)
|
|
{
|
|
g_slice_free (TrackObjIters, iters);
|
|
}
|
|
|
|
/* The move context is used for the timeline editing modes functions in order to
|
|
* + Ripple / Roll / Slide / Move / Trim
|
|
*
|
|
* The context aims at avoiding to recalculate values/objects on each call of the
|
|
* editing functions.
|
|
*/
|
|
struct _MoveContext
|
|
{
|
|
GESClip *clip;
|
|
GESEdge edge;
|
|
GESEditMode mode;
|
|
|
|
/* The start of the moving context */
|
|
GstClockTime start;
|
|
|
|
/* Ripple and Roll Objects */
|
|
GList *moving_trackelements;
|
|
|
|
/* We use it as a set of Clip to move between layers */
|
|
GHashTable *toplevel_containers;
|
|
/* Min priority of the objects currently in toplevel_containers */
|
|
guint min_move_layer;
|
|
/* Max priority of the objects currently in toplevel_containers */
|
|
guint max_layer_prio;
|
|
|
|
/* Never trim so duration would become < 0 */
|
|
guint64 max_trim_pos;
|
|
|
|
/* Never trim so inpoint + duration would change */
|
|
guint64 min_trim_pos;
|
|
|
|
/* fields to force/avoid new context */
|
|
/* Set to %TRUE when the track is doing updates of track element
|
|
* properties so we don't end up always needing new move context */
|
|
gboolean ignore_needs_ctx;
|
|
gboolean needs_move_ctx;
|
|
|
|
/* Last snapping properties */
|
|
GESTrackElement *last_snaped1;
|
|
GESTrackElement *last_snaped2;
|
|
GstClockTime *last_snap_ts;
|
|
};
|
|
|
|
struct _GESTimelinePrivate
|
|
{
|
|
/* The duration of the timeline */
|
|
gint64 duration;
|
|
|
|
/* The auto-transition of the timeline */
|
|
gboolean auto_transition;
|
|
/* Use to determine that a edit action should be rolled
|
|
* back because it leads to a wrong state of the element
|
|
* position (currently only happens if 3 clips overlap) */
|
|
gboolean needs_rollback;
|
|
gboolean rolling_back;
|
|
|
|
/* Timeline edition modes and snapping management */
|
|
guint64 snapping_distance;
|
|
|
|
/* FIXME: Should we offer an API over those fields ?
|
|
* FIXME: Should other classes than subclasses of Source also
|
|
* be tracked? */
|
|
|
|
/* Snapping fields */
|
|
GHashTable *by_start; /* {Source: start} */
|
|
GHashTable *by_end; /* {Source: end} */
|
|
GHashTable *by_object; /* {timecode: Source} */
|
|
GHashTable *obj_iters; /* {Source: TrackObjIters} */
|
|
GSequence *starts_ends; /* Sorted list of starts/ends */
|
|
/* We keep 1 reference to our trackelement here */
|
|
GSequence *tracksources; /* Source-s sorted by start/priorities */
|
|
|
|
GRecMutex dyn_mutex;
|
|
GList *priv_tracks;
|
|
/* FIXME: We should definitly offer an API over this,
|
|
* probably through a ges_layer_get_track_elements () method */
|
|
GHashTable *by_layer; /* {layer: GSequence of TrackElement by start/priorities} */
|
|
|
|
GList *auto_transitions;
|
|
|
|
MoveContext movecontext;
|
|
|
|
/* This variable is set to %TRUE when it makes sense to update the transitions,
|
|
* and %FALSE otherwize */
|
|
gboolean needs_transitions_update;
|
|
|
|
/* While we are creating and adding the TrackElements for a clip, we need to
|
|
* ignore the child-added signal */
|
|
GESClip *ignore_track_element_added;
|
|
GList *groups;
|
|
|
|
guint group_id;
|
|
|
|
GHashTable *all_elements;
|
|
|
|
/* With GST_OBJECT_LOCK */
|
|
guint expected_async_done;
|
|
/* With GST_OBJECT_LOCK */
|
|
guint expected_commited;
|
|
|
|
/* For ges_timeline_commit_sync */
|
|
GMutex commited_lock;
|
|
GCond commited_cond;
|
|
};
|
|
|
|
/* private structure to contain our track-related information */
|
|
|
|
typedef struct
|
|
{
|
|
GESTimeline *timeline;
|
|
GESTrack *track;
|
|
GstPad *pad; /* Pad from the track */
|
|
GstPad *ghostpad;
|
|
|
|
gulong probe_id;
|
|
} TrackPrivate;
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_DURATION,
|
|
PROP_AUTO_TRANSITION,
|
|
PROP_SNAPPING_DISTANCE,
|
|
PROP_UPDATE,
|
|
PROP_LAST
|
|
};
|
|
|
|
static GParamSpec *properties[PROP_LAST];
|
|
|
|
enum
|
|
{
|
|
TRACK_ADDED,
|
|
TRACK_REMOVED,
|
|
LAYER_ADDED,
|
|
LAYER_REMOVED,
|
|
SNAPING_STARTED,
|
|
SNAPING_ENDED,
|
|
SELECT_TRACKS_FOR_OBJECT,
|
|
COMMITED,
|
|
LAST_SIGNAL
|
|
};
|
|
|
|
static GstBinClass *parent_class;
|
|
|
|
static guint ges_timeline_signals[LAST_SIGNAL] = { 0 };
|
|
|
|
static gint custom_find_track (TrackPrivate * tr_priv, GESTrack * track);
|
|
|
|
static guint nb_assets = 0;
|
|
|
|
/* GESExtractable implementation */
|
|
static gchar *
|
|
extractable_check_id (GType type, const gchar * id)
|
|
{
|
|
gchar *res;
|
|
|
|
if (id == NULL)
|
|
res = g_strdup_printf ("%s-%i", "project", nb_assets);
|
|
else
|
|
res = g_strdup (id);
|
|
|
|
nb_assets++;
|
|
|
|
return res;
|
|
}
|
|
|
|
static gchar *
|
|
extractable_get_id (GESExtractable * self)
|
|
{
|
|
GESAsset *asset;
|
|
|
|
if (!(asset = ges_extractable_get_asset (self)))
|
|
return NULL;
|
|
|
|
return g_strdup (ges_asset_get_id (asset));
|
|
}
|
|
|
|
static void
|
|
ges_extractable_interface_init (GESExtractableInterface * iface)
|
|
{
|
|
iface->asset_type = GES_TYPE_PROJECT;
|
|
iface->check_id = (GESExtractableCheckId) extractable_check_id;
|
|
iface->get_id = extractable_get_id;
|
|
}
|
|
|
|
static void
|
|
ges_meta_container_interface_init (GESMetaContainerInterface * iface)
|
|
{
|
|
}
|
|
|
|
/* GObject Standard vmethods*/
|
|
static void
|
|
ges_timeline_get_property (GObject * object, guint property_id,
|
|
GValue * value, GParamSpec * pspec)
|
|
{
|
|
GESTimeline *timeline = GES_TIMELINE (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_DURATION:
|
|
g_value_set_uint64 (value, timeline->priv->duration);
|
|
break;
|
|
case PROP_AUTO_TRANSITION:
|
|
g_value_set_boolean (value, timeline->priv->auto_transition);
|
|
break;
|
|
case PROP_SNAPPING_DISTANCE:
|
|
g_value_set_uint64 (value, timeline->priv->snapping_distance);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ges_timeline_set_property (GObject * object, guint property_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
GESTimeline *timeline = GES_TIMELINE (object);
|
|
|
|
switch (property_id) {
|
|
case PROP_AUTO_TRANSITION:
|
|
ges_timeline_set_auto_transition (timeline, g_value_get_boolean (value));
|
|
break;
|
|
case PROP_SNAPPING_DISTANCE:
|
|
timeline->priv->snapping_distance = g_value_get_uint64 (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ges_timeline_dispose (GObject * object)
|
|
{
|
|
GESTimeline *tl = GES_TIMELINE (object);
|
|
GESTimelinePrivate *priv = tl->priv;
|
|
|
|
while (tl->layers) {
|
|
GESLayer *layer = (GESLayer *) tl->layers->data;
|
|
ges_timeline_remove_layer (GES_TIMELINE (object), layer);
|
|
}
|
|
|
|
/* FIXME: it should be possible to remove tracks before removing
|
|
* layers, but at the moment this creates a problem because the track
|
|
* objects aren't notified that their nleobjects have been destroyed.
|
|
*/
|
|
|
|
while (tl->tracks)
|
|
ges_timeline_remove_track (GES_TIMELINE (object), tl->tracks->data);
|
|
|
|
while (priv->groups)
|
|
g_list_free_full (ges_container_ungroup (priv->groups->data, FALSE),
|
|
gst_object_unref);
|
|
|
|
g_hash_table_unref (priv->by_start);
|
|
g_hash_table_unref (priv->by_end);
|
|
g_hash_table_unref (priv->by_object);
|
|
g_hash_table_unref (priv->by_layer);
|
|
g_hash_table_unref (priv->obj_iters);
|
|
g_sequence_free (priv->starts_ends);
|
|
g_sequence_free (priv->tracksources);
|
|
g_list_free (priv->movecontext.moving_trackelements);
|
|
g_hash_table_unref (priv->movecontext.toplevel_containers);
|
|
|
|
g_list_free_full (priv->auto_transitions, gst_object_unref);
|
|
|
|
g_hash_table_unref (priv->all_elements);
|
|
|
|
G_OBJECT_CLASS (ges_timeline_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
ges_timeline_finalize (GObject * object)
|
|
{
|
|
GESTimeline *tl = GES_TIMELINE (object);
|
|
|
|
g_rec_mutex_clear (&tl->priv->dyn_mutex);
|
|
|
|
G_OBJECT_CLASS (ges_timeline_parent_class)->finalize (object);
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
ges_timeline_handle_message (GstBin * bin, GstMessage * message)
|
|
{
|
|
GESTimeline *timeline = GES_TIMELINE (bin);
|
|
|
|
if (GST_MESSAGE_TYPE (message) == GST_MESSAGE_ELEMENT) {
|
|
GstMessage *amessage = NULL;
|
|
const GstStructure *mstructure = gst_message_get_structure (message);
|
|
|
|
if (gst_structure_has_name (mstructure, "NleCompositionStartUpdate")) {
|
|
if (g_strcmp0 (gst_structure_get_string (mstructure, "reason"), "Seek")) {
|
|
GST_INFO_OBJECT (timeline,
|
|
"A composition is starting an update because of %s"
|
|
" not concidering async", gst_structure_get_string (mstructure,
|
|
"reason"));
|
|
|
|
goto forward;
|
|
}
|
|
|
|
GST_OBJECT_LOCK (timeline);
|
|
if (timeline->priv->expected_async_done == 0) {
|
|
amessage = gst_message_new_async_start (GST_OBJECT_CAST (bin));
|
|
timeline->priv->expected_async_done = g_list_length (timeline->tracks);
|
|
GST_INFO_OBJECT (timeline, "Posting ASYNC_START %s",
|
|
gst_structure_get_string (mstructure, "reason"));
|
|
}
|
|
GST_OBJECT_UNLOCK (timeline);
|
|
|
|
} else if (gst_structure_has_name (mstructure, "NleCompositionUpdateDone")) {
|
|
if (g_strcmp0 (gst_structure_get_string (mstructure, "reason"), "Seek")) {
|
|
GST_INFO_OBJECT (timeline,
|
|
"A composition is done updating because of %s"
|
|
" not concidering async", gst_structure_get_string (mstructure,
|
|
"reason"));
|
|
|
|
goto forward;
|
|
}
|
|
|
|
GST_OBJECT_LOCK (timeline);
|
|
timeline->priv->expected_async_done -= 1;
|
|
if (timeline->priv->expected_async_done == 0) {
|
|
amessage = gst_message_new_async_done (GST_OBJECT_CAST (bin),
|
|
GST_CLOCK_TIME_NONE);
|
|
GST_INFO_OBJECT (timeline, "Posting ASYNC_DONE %s",
|
|
gst_structure_get_string (mstructure, "reason"));
|
|
}
|
|
GST_OBJECT_UNLOCK (timeline);
|
|
}
|
|
|
|
if (amessage)
|
|
gst_element_post_message (GST_ELEMENT_CAST (bin), amessage);
|
|
}
|
|
|
|
forward:
|
|
gst_element_post_message (GST_ELEMENT_CAST (bin), message);
|
|
}
|
|
|
|
/* we collect the first result */
|
|
static gboolean
|
|
_gst_array_accumulator (GSignalInvocationHint * ihint,
|
|
GValue * return_accu, const GValue * handler_return, gpointer dummy)
|
|
{
|
|
gpointer array;
|
|
|
|
array = g_value_get_boxed (handler_return);
|
|
if (!(ihint->run_type & G_SIGNAL_RUN_CLEANUP))
|
|
g_value_set_boxed (return_accu, array);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
ges_timeline_class_init (GESTimelineClass * klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GstBinClass *bin_class = GST_BIN_CLASS (klass);
|
|
|
|
g_type_class_add_private (klass, sizeof (GESTimelinePrivate));
|
|
|
|
GST_DEBUG_CATEGORY_INIT (ges_timeline_debug, "gestimeline",
|
|
GST_DEBUG_FG_YELLOW, "ges timeline");
|
|
|
|
parent_class = g_type_class_peek_parent (klass);
|
|
|
|
object_class->get_property = ges_timeline_get_property;
|
|
object_class->set_property = ges_timeline_set_property;
|
|
object_class->dispose = ges_timeline_dispose;
|
|
object_class->finalize = ges_timeline_finalize;
|
|
|
|
bin_class->handle_message = GST_DEBUG_FUNCPTR (ges_timeline_handle_message);
|
|
|
|
/**
|
|
* GESTimeline:duration:
|
|
*
|
|
* Current duration (in nanoseconds) of the #GESTimeline
|
|
*/
|
|
properties[PROP_DURATION] =
|
|
g_param_spec_uint64 ("duration", "Duration",
|
|
"The duration of the timeline", 0, G_MAXUINT64,
|
|
GST_CLOCK_TIME_NONE, G_PARAM_READABLE);
|
|
g_object_class_install_property (object_class, PROP_DURATION,
|
|
properties[PROP_DURATION]);
|
|
|
|
/**
|
|
* GESTimeline:auto-transition:
|
|
*
|
|
* Sets whether transitions are added automagically when clips overlap.
|
|
*/
|
|
g_object_class_install_property (object_class, PROP_AUTO_TRANSITION,
|
|
g_param_spec_boolean ("auto-transition", "Auto-Transition",
|
|
"whether the transitions are added", FALSE, G_PARAM_READWRITE));
|
|
|
|
/**
|
|
* GESTimeline:snapping-distance:
|
|
*
|
|
* Distance (in nanoseconds) from which a moving object will snap
|
|
* with it neighboors. 0 means no snapping.
|
|
*/
|
|
properties[PROP_SNAPPING_DISTANCE] =
|
|
g_param_spec_uint64 ("snapping-distance", "Snapping distance",
|
|
"Distance from which moving an object will snap with neighboors", 0,
|
|
G_MAXUINT64, 0, G_PARAM_READWRITE);
|
|
g_object_class_install_property (object_class, PROP_SNAPPING_DISTANCE,
|
|
properties[PROP_SNAPPING_DISTANCE]);
|
|
|
|
/**
|
|
* GESTimeline::track-added:
|
|
* @timeline: the #GESTimeline
|
|
* @track: the #GESTrack that was added to the timeline
|
|
*
|
|
* Will be emitted after the track was added to the timeline.
|
|
*/
|
|
ges_timeline_signals[TRACK_ADDED] =
|
|
g_signal_new ("track-added", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESTimelineClass, track_added), NULL,
|
|
NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GES_TYPE_TRACK);
|
|
|
|
/**
|
|
* GESTimeline::track-removed:
|
|
* @timeline: the #GESTimeline
|
|
* @track: the #GESTrack that was removed from the timeline
|
|
*
|
|
* Will be emitted after the track was removed from the timeline.
|
|
*/
|
|
ges_timeline_signals[TRACK_REMOVED] =
|
|
g_signal_new ("track-removed", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESTimelineClass, track_removed),
|
|
NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GES_TYPE_TRACK);
|
|
|
|
/**
|
|
* GESTimeline::layer-added:
|
|
* @timeline: the #GESTimeline
|
|
* @layer: the #GESLayer that was added to the timeline
|
|
*
|
|
* Will be emitted after the layer was added to the timeline.
|
|
*/
|
|
ges_timeline_signals[LAYER_ADDED] =
|
|
g_signal_new ("layer-added", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESTimelineClass, layer_added), NULL,
|
|
NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GES_TYPE_LAYER);
|
|
|
|
/**
|
|
* GESTimeline::layer-removed:
|
|
* @timeline: the #GESTimeline
|
|
* @layer: the #GESLayer that was removed from the timeline
|
|
*
|
|
* Will be emitted after the layer was removed from the timeline.
|
|
*/
|
|
ges_timeline_signals[LAYER_REMOVED] =
|
|
g_signal_new ("layer-removed", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESTimelineClass, layer_removed),
|
|
NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GES_TYPE_LAYER);
|
|
|
|
/**
|
|
* GESTimeline::track-elements-snapping:
|
|
* @timeline: the #GESTimeline
|
|
* @obj1: the first #GESTrackElement that was snapping.
|
|
* @obj2: the second #GESTrackElement that was snapping.
|
|
* @position: the position where the two objects finally snapping.
|
|
*
|
|
* Will be emitted when the 2 #GESTrackElement first snapped
|
|
*/
|
|
ges_timeline_signals[SNAPING_STARTED] =
|
|
g_signal_new ("snapping-started", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
|
G_TYPE_NONE, 3, GES_TYPE_TRACK_ELEMENT, GES_TYPE_TRACK_ELEMENT,
|
|
G_TYPE_UINT64);
|
|
|
|
/**
|
|
* GESTimeline::snapping-end:
|
|
* @timeline: the #GESTimeline
|
|
* @obj1: the first #GESTrackElement that was snapping.
|
|
* @obj2: the second #GESTrackElement that was snapping.
|
|
* @position: the position where the two objects finally snapping.
|
|
*
|
|
* Will be emitted when the 2 #GESTrackElement ended to snap
|
|
*/
|
|
ges_timeline_signals[SNAPING_ENDED] =
|
|
g_signal_new ("snapping-ended", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
|
|
G_TYPE_NONE, 3, GES_TYPE_TRACK_ELEMENT, GES_TYPE_TRACK_ELEMENT,
|
|
G_TYPE_UINT64);
|
|
|
|
/**
|
|
* GESTimeline::select-tracks-for-object:
|
|
* @timeline: the #GESTimeline
|
|
* @clip: The #GESClip on which @track-element will land
|
|
* @track-element: The #GESTrackElement for which to choose the tracks it should land into
|
|
*
|
|
* Returns: (transfer full) (element-type GESTrack): a #GPtrArray of #GESTrack-s where that object should be added
|
|
*/
|
|
ges_timeline_signals[SELECT_TRACKS_FOR_OBJECT] =
|
|
g_signal_new ("select-tracks-for-object", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST, 0, _gst_array_accumulator, NULL, NULL,
|
|
G_TYPE_PTR_ARRAY, 2, GES_TYPE_CLIP, GES_TYPE_TRACK_ELEMENT);
|
|
|
|
/**
|
|
* GESTimeline::commited:
|
|
* @timeline: the #GESTimeline
|
|
*
|
|
* This signal will be emitted once the changes initiated by #ges_timeline_commit
|
|
* have been executed in the backend. Use #ges_timeline_commit_sync if you
|
|
* don't need to do anything in the meantime.
|
|
*/
|
|
ges_timeline_signals[COMMITED] =
|
|
g_signal_new ("commited", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
|
|
}
|
|
|
|
static void
|
|
ges_timeline_init (GESTimeline * self)
|
|
{
|
|
GESTimelinePrivate *priv = self->priv;
|
|
|
|
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
|
|
GES_TYPE_TIMELINE, GESTimelinePrivate);
|
|
|
|
priv = self->priv;
|
|
self->layers = NULL;
|
|
self->tracks = NULL;
|
|
self->priv->duration = 0;
|
|
self->priv->auto_transition = FALSE;
|
|
priv->snapping_distance = 0;
|
|
priv->expected_async_done = 0;
|
|
priv->expected_commited = 0;
|
|
|
|
/* Move context initialization */
|
|
init_movecontext (&self->priv->movecontext, TRUE);
|
|
priv->movecontext.ignore_needs_ctx = FALSE;
|
|
|
|
priv->priv_tracks = NULL;
|
|
priv->by_start = g_hash_table_new (g_direct_hash, g_direct_equal);
|
|
priv->by_end = g_hash_table_new (g_direct_hash, g_direct_equal);
|
|
priv->by_object = g_hash_table_new (g_direct_hash, g_direct_equal);
|
|
priv->by_layer = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
|
|
(GDestroyNotify) g_sequence_free);
|
|
priv->obj_iters = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
|
|
(GDestroyNotify) _destroy_obj_iters);
|
|
priv->starts_ends = g_sequence_new (g_free);
|
|
priv->tracksources = g_sequence_new (gst_object_unref);
|
|
|
|
priv->needs_transitions_update = TRUE;
|
|
|
|
priv->all_elements =
|
|
g_hash_table_new_full (g_str_hash, g_str_equal, g_free, gst_object_unref);
|
|
|
|
priv->group_id = -1;
|
|
|
|
g_signal_connect_after (self, "select-tracks-for-object",
|
|
G_CALLBACK (select_tracks_for_object_default), NULL);
|
|
|
|
g_rec_mutex_init (&priv->dyn_mutex);
|
|
g_mutex_init (&priv->commited_lock);
|
|
}
|
|
|
|
/* Private methods */
|
|
|
|
static inline GESContainer *
|
|
get_toplevel_container (GESTrackElement * element)
|
|
{
|
|
GESTimelineElement *ret =
|
|
ges_timeline_element_get_toplevel_parent ((GESTimelineElement
|
|
*) (element));
|
|
|
|
/* We own a ref to the elements ourself */
|
|
gst_object_unref (ret);
|
|
return (GESContainer *) ret;
|
|
}
|
|
|
|
/* Sorting utils*/
|
|
static gint
|
|
sort_layers (gpointer a, gpointer b)
|
|
{
|
|
GESLayer *layer_a, *layer_b;
|
|
guint prio_a, prio_b;
|
|
|
|
layer_a = GES_LAYER (a);
|
|
layer_b = GES_LAYER (b);
|
|
|
|
prio_a = ges_layer_get_priority (layer_a);
|
|
prio_b = ges_layer_get_priority (layer_b);
|
|
|
|
if ((gint) prio_a > (guint) prio_b)
|
|
return 1;
|
|
if ((guint) prio_a < (guint) prio_b)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
timeline_update_duration (GESTimeline * timeline)
|
|
{
|
|
GstClockTime *cduration;
|
|
GSequenceIter *it = g_sequence_get_end_iter (timeline->priv->starts_ends);
|
|
|
|
it = g_sequence_iter_prev (it);
|
|
|
|
if (g_sequence_iter_is_end (it)) {
|
|
timeline->priv->duration = 0;
|
|
g_object_notify_by_pspec (G_OBJECT (timeline), properties[PROP_DURATION]);
|
|
return;
|
|
}
|
|
|
|
cduration = g_sequence_get (it);
|
|
|
|
if (cduration && timeline->priv->duration != *cduration) {
|
|
GST_DEBUG ("track duration : %" GST_TIME_FORMAT " current : %"
|
|
GST_TIME_FORMAT, GST_TIME_ARGS (*cduration),
|
|
GST_TIME_ARGS (timeline->priv->duration));
|
|
|
|
timeline->priv->duration = *cduration;
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (timeline), properties[PROP_DURATION]);
|
|
}
|
|
}
|
|
|
|
static gint
|
|
find_layer_by_prio (GESLayer * a, gpointer pprio)
|
|
{
|
|
gint prio = GPOINTER_TO_INT (pprio), lprio = ges_layer_get_priority (a);
|
|
|
|
if (lprio < prio)
|
|
return -1;
|
|
if (lprio > prio)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
sort_track_elements (GESTimeline * timeline, TrackObjIters * iters)
|
|
{
|
|
g_sequence_sort_changed (iters->iter_obj,
|
|
(GCompareDataFunc) element_start_compare, NULL);
|
|
}
|
|
|
|
static gint
|
|
compare_uint64 (guint64 * a, guint64 * b, gpointer user_data)
|
|
{
|
|
if (*a > *b)
|
|
return 1;
|
|
else if (*a == *b)
|
|
return 0;
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
static gint
|
|
custom_find_track (TrackPrivate * tr_priv, GESTrack * track)
|
|
{
|
|
if (tr_priv->track == track)
|
|
return 0;
|
|
return -1;
|
|
}
|
|
|
|
static inline void
|
|
sort_starts_ends_end (GESTimeline * timeline, TrackObjIters * iters)
|
|
{
|
|
GESTimelineElement *obj = GES_TIMELINE_ELEMENT (iters->trackelement);
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
guint64 *end = g_hash_table_lookup (priv->by_end, obj);
|
|
|
|
*end = _START (obj) + _DURATION (obj);
|
|
|
|
g_sequence_sort_changed (iters->iter_end, (GCompareDataFunc) compare_uint64,
|
|
NULL);
|
|
timeline_update_duration (timeline);
|
|
}
|
|
|
|
static inline void
|
|
sort_starts_ends_start (GESTimeline * timeline, TrackObjIters * iters)
|
|
{
|
|
GESTimelineElement *obj = GES_TIMELINE_ELEMENT (iters->trackelement);
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
guint64 *start = g_hash_table_lookup (priv->by_start, obj);
|
|
|
|
*start = _START (obj);
|
|
|
|
g_sequence_sort_changed (iters->iter_start,
|
|
(GCompareDataFunc) compare_uint64, NULL);
|
|
timeline_update_duration (timeline);
|
|
}
|
|
|
|
static void
|
|
_destroy_auto_transition_cb (GESAutoTransition * auto_transition,
|
|
GESTimeline * timeline)
|
|
{
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
GESClip *transition = auto_transition->transition_clip;
|
|
GESLayer *layer = ges_clip_get_layer (transition);
|
|
|
|
ges_layer_remove_clip (layer, transition);
|
|
g_signal_handlers_disconnect_by_func (auto_transition,
|
|
_destroy_auto_transition_cb, timeline);
|
|
|
|
|
|
priv->auto_transitions =
|
|
g_list_remove (priv->auto_transitions, auto_transition);
|
|
gst_object_unref (auto_transition);
|
|
}
|
|
|
|
static GESAutoTransition *
|
|
create_transition (GESTimeline * timeline, GESTrackElement * previous,
|
|
GESTrackElement * next, GESClip * transition,
|
|
GESLayer * layer, guint64 start, guint64 duration)
|
|
{
|
|
GESAsset *asset;
|
|
GESAutoTransition *auto_transition;
|
|
|
|
if (transition == NULL) {
|
|
/* TODO make it possible to specify a Transition asset in the API */
|
|
asset = ges_asset_request (GES_TYPE_TRANSITION_CLIP, "crossfade", NULL);
|
|
transition =
|
|
ges_layer_add_asset (layer, asset, start, 0, duration,
|
|
ges_track_element_get_track_type (next));
|
|
} else {
|
|
GST_DEBUG_OBJECT (timeline,
|
|
"Reusing already existing transition: %" GST_PTR_FORMAT, transition);
|
|
}
|
|
|
|
/* We know there is only 1 TrackElement */
|
|
auto_transition =
|
|
ges_auto_transition_new (GES_CONTAINER_CHILDREN (transition)->data,
|
|
previous, next);
|
|
|
|
g_signal_connect (auto_transition, "destroy-me",
|
|
G_CALLBACK (_destroy_auto_transition_cb), timeline);
|
|
|
|
timeline->priv->auto_transitions =
|
|
g_list_prepend (timeline->priv->auto_transitions, auto_transition);
|
|
|
|
return auto_transition;
|
|
}
|
|
|
|
typedef GESAutoTransition *(*GetAutoTransitionFunc) (GESTimeline * timeline,
|
|
GESLayer * layer, GESTrack * track, GESTrackElement * previous,
|
|
GESTrackElement * next, GstClockTime transition_duration);
|
|
|
|
static GESAutoTransition *
|
|
_find_transition_from_auto_transitions (GESTimeline * timeline,
|
|
GESLayer * layer, GESTrack * track, GESTrackElement * prev,
|
|
GESTrackElement * next, GstClockTime transition_duration)
|
|
{
|
|
GList *tmp;
|
|
|
|
for (tmp = timeline->priv->auto_transitions; tmp; tmp = tmp->next) {
|
|
GESAutoTransition *auto_trans = (GESAutoTransition *) tmp->data;
|
|
|
|
/* We already have a transition linked to one of the elements we want to
|
|
* find a transition for */
|
|
if (auto_trans->previous_source == prev || auto_trans->next_source == next) {
|
|
if (auto_trans->previous_source != prev
|
|
|| auto_trans->next_source != next) {
|
|
timeline->priv->needs_rollback = TRUE;
|
|
GST_INFO_OBJECT (timeline, "Failed creating auto transition, "
|
|
" trying to have 3 clips overlapping, rolling back");
|
|
}
|
|
|
|
return auto_trans;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static GESAutoTransition *
|
|
_create_auto_transition_from_transitions (GESTimeline * timeline,
|
|
GESLayer * layer, GESTrack * track, GESTrackElement * prev,
|
|
GESTrackElement * next, GstClockTime transition_duration)
|
|
{
|
|
GSequenceIter *tmp_iter;
|
|
GSequence *by_layer_sequence;
|
|
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
GESAutoTransition *auto_transition =
|
|
_find_transition_from_auto_transitions (timeline, layer, track, prev,
|
|
next, transition_duration);
|
|
|
|
if (auto_transition)
|
|
return auto_transition;
|
|
|
|
|
|
/* Try to find a transition that perfectly fits with the one that
|
|
* should be added at that place
|
|
* optimize: Use g_sequence_search instead of going over all the
|
|
* sequence */
|
|
by_layer_sequence = g_hash_table_lookup (priv->by_layer, layer);
|
|
for (tmp_iter = g_sequence_get_begin_iter (by_layer_sequence);
|
|
tmp_iter && !g_sequence_iter_is_end (tmp_iter);
|
|
tmp_iter = g_sequence_iter_next (tmp_iter)) {
|
|
GESTrackElement *maybe_transition = g_sequence_get (tmp_iter);
|
|
|
|
if (ges_track_element_get_track (maybe_transition) != track)
|
|
continue;
|
|
|
|
if (_START (maybe_transition) > _START (next))
|
|
break;
|
|
else if (_START (maybe_transition) != _START (next) ||
|
|
_DURATION (maybe_transition) != transition_duration)
|
|
continue;
|
|
else if (GES_IS_TRANSITION (maybe_transition))
|
|
/* Use that transition */
|
|
/* TODO We should make sure that the transition contains only
|
|
* TrackElement-s in @track and if it is not the case properly unlink the
|
|
* object to use it */
|
|
return create_transition (timeline, prev, next,
|
|
GES_CLIP (GES_TIMELINE_ELEMENT_PARENT (maybe_transition)), layer,
|
|
_START (next), transition_duration);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Create all transition that do not exist on @layer.
|
|
* @get_auto_transition is called to check if a particular transition exists
|
|
* if @ track is specified, we will create the transitions only for that particular
|
|
* track */
|
|
static void
|
|
_create_transitions_on_layer (GESTimeline * timeline, GESLayer * layer,
|
|
GESTrack * track, GESTrackElement * initiating_obj,
|
|
GetAutoTransitionFunc get_auto_transition)
|
|
{
|
|
guint32 layer_prio;
|
|
GSequenceIter *iter;
|
|
GESAutoTransition *transition;
|
|
|
|
GESTrack *ctrack = track;
|
|
GList *entered = NULL; /* List of TrackElement for wich we walk through the
|
|
* "start" but not the "end" in the starts_ends list */
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
|
|
if (!layer || !ges_layer_get_auto_transition (layer))
|
|
return;
|
|
|
|
layer_prio = ges_layer_get_priority (layer);
|
|
for (iter = g_sequence_get_begin_iter (priv->starts_ends);
|
|
iter && !g_sequence_iter_is_end (iter);
|
|
iter = g_sequence_iter_next (iter)) {
|
|
GList *tmp;
|
|
guint *start_or_end = g_sequence_get (iter);
|
|
GESTrackElement *next = g_hash_table_lookup (timeline->priv->by_object,
|
|
start_or_end);
|
|
GESTimelineElement *toplevel =
|
|
ges_timeline_element_get_toplevel_parent (GES_TIMELINE_ELEMENT (next));
|
|
|
|
/* Only object that are in that layer and track */
|
|
if (_ges_track_element_get_layer_priority (next) != layer_prio ||
|
|
(track && track != ges_track_element_get_track (next)))
|
|
continue;
|
|
|
|
if (track == NULL)
|
|
ctrack = ges_track_element_get_track (next);
|
|
|
|
if (start_or_end == g_hash_table_lookup (priv->by_end, next)) {
|
|
if (initiating_obj == next) {
|
|
/* We passed the objects that initiated the research
|
|
* we are now done */
|
|
g_list_free (entered);
|
|
return;
|
|
}
|
|
entered = g_list_remove (entered, next);
|
|
|
|
continue;
|
|
}
|
|
|
|
for (tmp = entered; tmp; tmp = tmp->next) {
|
|
gint64 transition_duration;
|
|
|
|
GESTrackElement *prev = tmp->data;
|
|
|
|
if (ctrack != ges_track_element_get_track (prev) ||
|
|
ges_timeline_element_get_toplevel_parent (GES_TIMELINE_ELEMENT (prev))
|
|
== toplevel)
|
|
continue;
|
|
|
|
if (priv->movecontext.moving_trackelements &&
|
|
GES_TIMELINE_ELEMENT_START (next) > priv->movecontext.start)
|
|
continue;
|
|
|
|
transition_duration = (_START (prev) + _DURATION (prev)) - _START (next);
|
|
if (transition_duration > 0 && transition_duration < _DURATION (prev) &&
|
|
transition_duration < _DURATION (next)) {
|
|
transition =
|
|
get_auto_transition (timeline, layer, ctrack, prev, next,
|
|
transition_duration);
|
|
if (!transition)
|
|
create_transition (timeline, prev, next, NULL, layer,
|
|
_START (next), transition_duration);
|
|
}
|
|
}
|
|
|
|
/* And add that object to the entered list so that it we can possibly set
|
|
* a transition on its end edge */
|
|
entered = g_list_append (entered, next);
|
|
}
|
|
}
|
|
|
|
/* @track_element must be a GESSource */
|
|
static void
|
|
create_transitions (GESTimeline * timeline, GESTrackElement * track_element)
|
|
{
|
|
GESTrack *track;
|
|
GList *layer_node;
|
|
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
|
|
if (!priv->needs_transitions_update)
|
|
return;
|
|
|
|
if (mv_ctx->moving_trackelements &&
|
|
GES_TIMELINE_ELEMENT_START (track_element) > mv_ctx->start) {
|
|
GST_DEBUG_OBJECT (timeline, "Not creating transition around %"
|
|
GES_TIMELINE_ELEMENT_FORMAT " as it is not the first rippled"
|
|
" element", GES_TIMELINE_ELEMENT_ARGS (track_element));
|
|
return;
|
|
}
|
|
|
|
track = ges_track_element_get_track (track_element);
|
|
layer_node = g_list_find_custom (timeline->layers,
|
|
GINT_TO_POINTER (_ges_track_element_get_layer_priority (track_element)),
|
|
(GCompareFunc) find_layer_by_prio);
|
|
|
|
_create_transitions_on_layer (timeline,
|
|
layer_node ? layer_node->data : NULL, track, track_element,
|
|
_find_transition_from_auto_transitions);
|
|
|
|
GST_DEBUG_OBJECT (timeline, "Done updating transitions");
|
|
}
|
|
|
|
/* Timeline edition functions */
|
|
static inline void
|
|
init_movecontext (MoveContext * mv_ctx, gboolean first_init)
|
|
{
|
|
if (G_UNLIKELY (first_init))
|
|
mv_ctx->toplevel_containers =
|
|
g_hash_table_new (g_direct_hash, g_direct_equal);
|
|
|
|
mv_ctx->moving_trackelements = NULL;
|
|
mv_ctx->start = G_MAXUINT64;
|
|
mv_ctx->max_trim_pos = G_MAXUINT64;
|
|
mv_ctx->min_move_layer = G_MAXUINT;
|
|
mv_ctx->max_layer_prio = 0;
|
|
mv_ctx->last_snaped1 = NULL;
|
|
mv_ctx->last_snaped2 = NULL;
|
|
mv_ctx->last_snap_ts = NULL;
|
|
}
|
|
|
|
static inline void
|
|
clean_movecontext (MoveContext * mv_ctx)
|
|
{
|
|
g_list_free (mv_ctx->moving_trackelements);
|
|
g_hash_table_remove_all (mv_ctx->toplevel_containers);
|
|
init_movecontext (mv_ctx, FALSE);
|
|
}
|
|
|
|
static void
|
|
stop_tracking_track_element (GESTimeline * timeline,
|
|
GESTrackElement * trackelement)
|
|
{
|
|
guint64 *start, *end;
|
|
TrackObjIters *iters;
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
|
|
iters = g_hash_table_lookup (priv->obj_iters, trackelement);
|
|
if (G_LIKELY (iters->iter_by_layer)) {
|
|
g_sequence_remove (iters->iter_by_layer);
|
|
} else {
|
|
GST_WARNING_OBJECT (timeline, "TrackElement %p was in no layer",
|
|
trackelement);
|
|
}
|
|
|
|
if (GES_IS_SOURCE (trackelement)) {
|
|
start = g_hash_table_lookup (priv->by_start, trackelement);
|
|
end = g_hash_table_lookup (priv->by_end, trackelement);
|
|
|
|
g_hash_table_remove (priv->by_start, trackelement);
|
|
g_hash_table_remove (priv->by_end, trackelement);
|
|
g_hash_table_remove (priv->by_object, end);
|
|
g_hash_table_remove (priv->by_object, start);
|
|
g_sequence_remove (iters->iter_start);
|
|
g_sequence_remove (iters->iter_end);
|
|
g_sequence_remove (iters->iter_obj);
|
|
timeline_update_duration (timeline);
|
|
}
|
|
g_hash_table_remove (priv->obj_iters, trackelement);
|
|
}
|
|
|
|
static void
|
|
start_tracking_track_element (GESTimeline * timeline,
|
|
GESTrackElement * trackelement)
|
|
{
|
|
guint64 *pstart, *pend;
|
|
GSequence *by_layer_sequence;
|
|
TrackObjIters *iters;
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
|
|
guint layer_prio = _ges_track_element_get_layer_priority (trackelement);
|
|
GList *layer_node = g_list_find_custom (timeline->layers,
|
|
GINT_TO_POINTER (layer_prio), (GCompareFunc) find_layer_by_prio);
|
|
GESLayer *layer = layer_node ? layer_node->data : NULL;
|
|
|
|
iters = g_slice_new0 (TrackObjIters);
|
|
|
|
/* We add all TrackElement to obj_iters as we always follow them
|
|
* in the by_layer Sequences */
|
|
g_hash_table_insert (priv->obj_iters, trackelement, iters);
|
|
|
|
/* Track all objects by layer */
|
|
if (G_UNLIKELY (layer == NULL)) {
|
|
/* We handle the case where we have TrackElement that are in no layer by not
|
|
* tracking them
|
|
*
|
|
* FIXME: Check if we should rather try to track them in some sort of
|
|
* dummy layer, or even refuse TrackElements to be added in Tracks if
|
|
* they land in no layer the timeline controls.
|
|
*/
|
|
GST_ERROR_OBJECT (timeline, "Adding a TrackElement that lands in no layer "
|
|
"we are controlling");
|
|
} else {
|
|
by_layer_sequence = g_hash_table_lookup (priv->by_layer, layer);
|
|
iters->iter_by_layer =
|
|
g_sequence_insert_sorted (by_layer_sequence, trackelement,
|
|
(GCompareDataFunc) element_start_compare, NULL);
|
|
iters->layer = layer;
|
|
}
|
|
|
|
if (GES_IS_SOURCE (trackelement)) {
|
|
/* Track only sources for timeline edition and snapping */
|
|
pstart = g_malloc (sizeof (guint64));
|
|
pend = g_malloc (sizeof (guint64));
|
|
*pstart = _START (trackelement);
|
|
*pend = *pstart + _DURATION (trackelement);
|
|
|
|
iters->iter_start = g_sequence_insert_sorted (priv->starts_ends, pstart,
|
|
(GCompareDataFunc) compare_uint64, NULL);
|
|
iters->iter_end = g_sequence_insert_sorted (priv->starts_ends, pend,
|
|
(GCompareDataFunc) compare_uint64, NULL);
|
|
iters->iter_obj =
|
|
g_sequence_insert_sorted (priv->tracksources,
|
|
gst_object_ref (trackelement), (GCompareDataFunc) element_start_compare,
|
|
NULL);
|
|
iters->trackelement = trackelement;
|
|
|
|
g_hash_table_insert (priv->by_start, trackelement, pstart);
|
|
g_hash_table_insert (priv->by_object, pstart, trackelement);
|
|
g_hash_table_insert (priv->by_end, trackelement, pend);
|
|
g_hash_table_insert (priv->by_object, pend, trackelement);
|
|
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
|
|
timeline_update_duration (timeline);
|
|
create_transitions (timeline, trackelement);
|
|
}
|
|
}
|
|
|
|
static inline void
|
|
ges_timeline_emit_snappig (GESTimeline * timeline, GESTrackElement * obj1,
|
|
guint64 * timecode)
|
|
{
|
|
GESTrackElement *obj2;
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
GstClockTime snap_time = timecode ? *timecode : 0;
|
|
GstClockTime last_snap_ts = mv_ctx->last_snap_ts ?
|
|
*mv_ctx->last_snap_ts : GST_CLOCK_TIME_NONE;
|
|
|
|
GST_DEBUG_OBJECT (timeline, "Distance: %" GST_TIME_FORMAT " snapping at %"
|
|
GST_TIME_FORMAT, GST_TIME_ARGS (timeline->priv->snapping_distance),
|
|
GST_TIME_ARGS (snap_time));
|
|
|
|
if (timecode == NULL) {
|
|
if (mv_ctx->last_snaped1 != NULL && mv_ctx->last_snaped2 != NULL) {
|
|
g_signal_emit (timeline, ges_timeline_signals[SNAPING_ENDED], 0,
|
|
mv_ctx->last_snaped1, mv_ctx->last_snaped2, last_snap_ts);
|
|
|
|
/* We then need to recalculate the moving context */
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
obj2 = g_hash_table_lookup (timeline->priv->by_object, timecode);
|
|
|
|
if (last_snap_ts != *timecode) {
|
|
g_signal_emit (timeline, ges_timeline_signals[SNAPING_ENDED], 0,
|
|
mv_ctx->last_snaped1, mv_ctx->last_snaped2, (last_snap_ts));
|
|
|
|
/* We want the snap start signal to be emited anyway */
|
|
mv_ctx->last_snap_ts = NULL;
|
|
}
|
|
|
|
if (mv_ctx->last_snap_ts == NULL) {
|
|
|
|
mv_ctx->last_snaped1 = obj1;
|
|
mv_ctx->last_snaped2 = obj2;
|
|
mv_ctx->last_snap_ts = timecode;
|
|
|
|
g_signal_emit (timeline, ges_timeline_signals[SNAPING_STARTED], 0,
|
|
obj1, obj2, *timecode);
|
|
|
|
}
|
|
}
|
|
|
|
static guint64 *
|
|
ges_timeline_snap_position (GESTimeline * timeline,
|
|
GESTrackElement * trackelement, guint64 * current, guint64 timecode,
|
|
gboolean emit)
|
|
{
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
GSequenceIter *iter, *prev_iter, *nxt_iter;
|
|
GESTrackElement *tmp_trackelement;
|
|
GESContainer *tmp_container, *container;
|
|
|
|
GstClockTime *last_snap_ts = priv->movecontext.last_snap_ts;
|
|
guint64 snap_distance = timeline->priv->snapping_distance;
|
|
guint64 *prev_tc, *next_tc, *ret = NULL, off = G_MAXUINT64, off1 =
|
|
G_MAXUINT64;
|
|
|
|
/* Avoid useless calculations */
|
|
if (snap_distance == 0)
|
|
return NULL;
|
|
|
|
/* If we can just resnap as last snap... do it */
|
|
if (last_snap_ts) {
|
|
off = timecode > *last_snap_ts ?
|
|
timecode - *last_snap_ts : *last_snap_ts - timecode;
|
|
if (off <= snap_distance) {
|
|
ret = last_snap_ts;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
container = get_toplevel_container (trackelement);
|
|
|
|
iter = g_sequence_search (priv->starts_ends, &timecode,
|
|
(GCompareDataFunc) compare_uint64, NULL);
|
|
|
|
/* Getting the next/previous values, and use the closest one if any "respects"
|
|
* the snap_distance value */
|
|
nxt_iter = iter;
|
|
while (!g_sequence_iter_is_end (nxt_iter)) {
|
|
next_tc = g_sequence_get (iter);
|
|
tmp_trackelement = g_hash_table_lookup (timeline->priv->by_object, next_tc);
|
|
tmp_container = get_toplevel_container (tmp_trackelement);
|
|
|
|
off = timecode > *next_tc ? timecode - *next_tc : *next_tc - timecode;
|
|
if (next_tc != current && off <= snap_distance
|
|
&& container != tmp_container) {
|
|
|
|
ret = next_tc;
|
|
break;
|
|
}
|
|
|
|
nxt_iter = g_sequence_iter_next (nxt_iter);
|
|
}
|
|
|
|
if (ret == NULL)
|
|
off = G_MAXUINT64;
|
|
|
|
if (priv->movecontext.moving_trackelements) {
|
|
GST_INFO_OBJECT (timeline, "Rippling, no way we snap end");
|
|
goto done;
|
|
}
|
|
|
|
prev_iter = g_sequence_iter_prev (iter);
|
|
while (!g_sequence_iter_is_begin (prev_iter)) {
|
|
prev_tc = g_sequence_get (prev_iter);
|
|
tmp_trackelement = g_hash_table_lookup (timeline->priv->by_object, prev_tc);
|
|
tmp_container = get_toplevel_container (tmp_trackelement);
|
|
|
|
off1 = timecode > *prev_tc ? timecode - *prev_tc : *prev_tc - timecode;
|
|
if (prev_tc != current && off1 < off && off1 <= snap_distance &&
|
|
container != tmp_container) {
|
|
ret = prev_tc;
|
|
|
|
break;
|
|
}
|
|
|
|
prev_iter = g_sequence_iter_prev (prev_iter);
|
|
}
|
|
|
|
done:
|
|
/* We emit the snapping signal only if we snapped with a different value
|
|
* than the current one */
|
|
if (emit) {
|
|
GstClockTime snap_time = ret ? *ret : GST_CLOCK_TIME_NONE;
|
|
|
|
if (!timeline->priv->needs_rollback)
|
|
ges_timeline_emit_snappig (timeline, trackelement, ret);
|
|
else
|
|
ges_timeline_emit_snappig (timeline, trackelement, NULL);
|
|
|
|
GST_DEBUG_OBJECT (timeline, "Snaping at %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (snap_time));
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static inline GESContainer *
|
|
add_toplevel_container (MoveContext * mv_ctx, GESTrackElement * trackelement)
|
|
{
|
|
guint layer_prio;
|
|
GESContainer *toplevel = get_toplevel_container (trackelement);
|
|
|
|
/* Avoid recalculating */
|
|
if (!g_hash_table_lookup (mv_ctx->toplevel_containers, toplevel)) {
|
|
if (GES_IS_CLIP (toplevel)) {
|
|
|
|
layer_prio = ges_clip_get_layer_priority (GES_CLIP (toplevel));
|
|
if (layer_prio == (guint32) - 1) {
|
|
GST_WARNING_OBJECT (toplevel, "Not in any layer, can not move"
|
|
" between layers");
|
|
|
|
return toplevel;
|
|
}
|
|
mv_ctx->min_move_layer = MIN (mv_ctx->min_move_layer, layer_prio);
|
|
mv_ctx->max_layer_prio = MAX (mv_ctx->max_layer_prio, layer_prio);
|
|
} else if GES_IS_GROUP
|
|
(toplevel) {
|
|
mv_ctx->min_move_layer = MIN (mv_ctx->min_move_layer,
|
|
_PRIORITY (toplevel));
|
|
mv_ctx->max_layer_prio = MAX (mv_ctx->max_layer_prio,
|
|
_PRIORITY (toplevel) + GES_CONTAINER_HEIGHT (toplevel));
|
|
} else
|
|
g_assert_not_reached ();
|
|
|
|
mv_ctx->start = MIN (mv_ctx->start, GES_TIMELINE_ELEMENT_START (toplevel));
|
|
g_hash_table_insert (mv_ctx->toplevel_containers, toplevel, toplevel);
|
|
|
|
}
|
|
|
|
return toplevel;
|
|
}
|
|
|
|
static gboolean
|
|
ges_move_context_set_objects (GESTimeline * timeline, GESTrackElement * obj,
|
|
GESEdge edge)
|
|
{
|
|
TrackObjIters *iters;
|
|
GESTrackElement *tmptrackelement;
|
|
guint64 start, end, tmpend;
|
|
GSequenceIter *iter, *trackelement_iter;
|
|
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
|
|
iters = g_hash_table_lookup (timeline->priv->obj_iters, obj);
|
|
trackelement_iter = iters->iter_obj;
|
|
switch (edge) {
|
|
case GES_EDGE_START:
|
|
/* set it properly in the context of "trimming" */
|
|
mv_ctx->max_trim_pos = 0;
|
|
mv_ctx->min_trim_pos = 0;
|
|
start = _START (obj);
|
|
|
|
if (g_sequence_iter_is_begin (trackelement_iter))
|
|
break;
|
|
|
|
/* Look for the objects */
|
|
for (iter = g_sequence_iter_prev (trackelement_iter);
|
|
iter && !g_sequence_iter_is_end (iter);
|
|
iter = g_sequence_iter_prev (iter)) {
|
|
|
|
tmptrackelement = GES_TRACK_ELEMENT (g_sequence_get (iter));
|
|
tmpend = _START (tmptrackelement) + _DURATION (tmptrackelement);
|
|
|
|
if (tmpend <= start) {
|
|
mv_ctx->max_trim_pos =
|
|
MAX (mv_ctx->max_trim_pos, _START (tmptrackelement));
|
|
mv_ctx->min_trim_pos = MAX (mv_ctx->min_trim_pos,
|
|
_START (tmptrackelement) - _INPOINT (tmptrackelement));
|
|
mv_ctx->moving_trackelements =
|
|
g_list_prepend (mv_ctx->moving_trackelements, tmptrackelement);
|
|
}
|
|
|
|
|
|
if (g_sequence_iter_is_begin (iter))
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case GES_EDGE_END:
|
|
case GES_EDGE_NONE: /* In this case only works for ripple */
|
|
end = _START (obj) + _DURATION (obj);
|
|
mv_ctx->max_trim_pos = G_MAXUINT64;
|
|
|
|
/* Look for folowing objects */
|
|
for (iter = g_sequence_iter_next (trackelement_iter);
|
|
iter && !g_sequence_iter_is_end (iter);
|
|
iter = g_sequence_iter_next (iter)) {
|
|
tmptrackelement = GES_TRACK_ELEMENT (g_sequence_get (iter));
|
|
|
|
if (_START (tmptrackelement) >= end) {
|
|
tmpend = _START (tmptrackelement) + _DURATION (tmptrackelement);
|
|
mv_ctx->max_trim_pos = MIN (mv_ctx->max_trim_pos, tmpend);
|
|
mv_ctx->moving_trackelements =
|
|
g_list_prepend (mv_ctx->moving_trackelements, tmptrackelement);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
GST_DEBUG ("Edge type %d no supported", edge);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
ges_timeline_set_moving_context (GESTimeline * timeline, GESTrackElement * obj,
|
|
GESEditMode mode, GESEdge edge, GList * layers)
|
|
{
|
|
/* A TrackElement that could initiate movement for other object */
|
|
GESTrackElement *editor_trackelement = NULL;
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
GESClip *clip = GES_CLIP (GES_TIMELINE_ELEMENT_PARENT (obj));
|
|
|
|
/* Still in the same mv_ctx */
|
|
if ((mv_ctx->clip == clip && mv_ctx->mode == mode &&
|
|
mv_ctx->edge == edge && !mv_ctx->needs_move_ctx)) {
|
|
|
|
GST_DEBUG ("Keeping the same moving mv_ctx");
|
|
return TRUE;
|
|
}
|
|
|
|
GST_DEBUG_OBJECT (clip,
|
|
"Changing context:\nold: obj: %p, mode: %d, edge: %d \n"
|
|
"new: obj: %p, mode: %d, edge: %d ! Has changed %i", mv_ctx->clip,
|
|
mv_ctx->mode, mv_ctx->edge, clip, mode, edge, mv_ctx->needs_move_ctx);
|
|
|
|
clean_movecontext (mv_ctx);
|
|
mv_ctx->edge = edge;
|
|
mv_ctx->mode = mode;
|
|
mv_ctx->clip = clip;
|
|
mv_ctx->needs_move_ctx = FALSE;
|
|
|
|
/* We try to find a Source inside the Clip so we can set the
|
|
* moving context Else we just move the selected one only */
|
|
if (GES_IS_SOURCE (obj) == FALSE) {
|
|
GList *tmp;
|
|
|
|
for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next) {
|
|
if (GES_IS_SOURCE (tmp->data)) {
|
|
editor_trackelement = tmp->data;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
editor_trackelement = obj;
|
|
}
|
|
|
|
if (editor_trackelement) {
|
|
switch (mode) {
|
|
case GES_EDIT_MODE_RIPPLE:
|
|
case GES_EDIT_MODE_ROLL:
|
|
if (!(ges_move_context_set_objects (timeline, editor_trackelement,
|
|
edge)))
|
|
return FALSE;
|
|
default:
|
|
break;
|
|
}
|
|
add_toplevel_container (&timeline->priv->movecontext, editor_trackelement);
|
|
} else {
|
|
/* We add the main object to the toplevel_containers set */
|
|
add_toplevel_container (&timeline->priv->movecontext, obj);
|
|
}
|
|
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
_trim_transition (GESTimeline * timeline, GESLayer * layer,
|
|
GESTrackElement * element, GESEdge edge, GstClockTime position)
|
|
{
|
|
|
|
GList *tmp;
|
|
|
|
if (!ges_layer_get_auto_transition (layer))
|
|
goto fail;
|
|
|
|
gst_object_unref (layer);
|
|
for (tmp = timeline->priv->auto_transitions; tmp; tmp = tmp->next) {
|
|
GESAutoTransition *auto_transition = tmp->data;
|
|
|
|
if (auto_transition->transition == GES_TRACK_ELEMENT (element)) {
|
|
/* Trimming an auto transition mean trimming its neighboors */
|
|
if (!auto_transition->positioning) {
|
|
if (edge == GES_EDGE_END) {
|
|
ges_container_edit (GES_CONTAINER (auto_transition->previous_clip),
|
|
NULL, -1, GES_EDIT_MODE_TRIM, GES_EDGE_END, position);
|
|
} else {
|
|
ges_container_edit (GES_CONTAINER (auto_transition->next_clip),
|
|
NULL, -1, GES_EDIT_MODE_TRIM, GES_EDGE_START, position);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
|
|
fail:
|
|
gst_object_unref (layer);
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
ges_timeline_trim_object_simple (GESTimeline * timeline,
|
|
GESTimelineElement * element, GList * layers, GESEdge edge,
|
|
guint64 position, gboolean snapping)
|
|
{
|
|
guint64 start, inpoint, duration, max_duration, *snapped, *cur;
|
|
gboolean ret = TRUE;
|
|
gint64 real_dur;
|
|
GESTrackElement *track_element;
|
|
|
|
if (GES_IS_TRANSITION (element)) {
|
|
return _trim_transition (timeline,
|
|
ges_clip_get_layer (GES_CLIP (GES_TIMELINE_ELEMENT_PARENT (element))),
|
|
GES_TRACK_ELEMENT (element), edge, position);
|
|
} else if (GES_IS_SOURCE (element) == FALSE) {
|
|
return FALSE;
|
|
}
|
|
|
|
track_element = GES_TRACK_ELEMENT (element);
|
|
GST_DEBUG_OBJECT (track_element, "Trimming to %" GST_TIME_FORMAT
|
|
" %s snaping, edge %i", GST_TIME_ARGS (position),
|
|
snapping ? "Is" : "Not", edge);
|
|
|
|
start = _START (track_element);
|
|
g_object_get (track_element, "max-duration", &max_duration, NULL);
|
|
|
|
switch (edge) {
|
|
case GES_EDGE_START:
|
|
{
|
|
GESTimelineElement *toplevel;
|
|
GESChildrenControlMode old_mode;
|
|
toplevel = ges_timeline_element_get_toplevel_parent (element);
|
|
|
|
if (position < _START (toplevel) && _START (toplevel) < _START (element)) {
|
|
GST_DEBUG_OBJECT (toplevel, "Not trimming %p as not at begining "
|
|
"of the container", element);
|
|
|
|
gst_object_unref (toplevel);
|
|
return FALSE;
|
|
}
|
|
|
|
old_mode = GES_CONTAINER (toplevel)->children_control_mode;
|
|
if (GES_IS_GROUP (toplevel) && old_mode == GES_CHILDREN_UPDATE) {
|
|
GST_DEBUG_OBJECT (toplevel, "Setting children udpate mode to"
|
|
" UPDDATE_ALL_VALUES so we can trim without moving the contained");
|
|
/* The container will update its values itself according to new
|
|
* values of the children */
|
|
GES_CONTAINER (toplevel)->children_control_mode =
|
|
GES_CHILDREN_UPDATE_ALL_VALUES;
|
|
}
|
|
|
|
inpoint = _INPOINT (track_element);
|
|
duration = _DURATION (track_element);
|
|
|
|
if (snapping) {
|
|
cur = g_hash_table_lookup (timeline->priv->by_start, track_element);
|
|
|
|
snapped = ges_timeline_snap_position (timeline, track_element, cur,
|
|
position, TRUE);
|
|
if (snapped)
|
|
position = *snapped;
|
|
}
|
|
|
|
/* Calculate new values */
|
|
position = MIN (position, start + duration);
|
|
|
|
if (inpoint + position < start) {
|
|
GST_INFO_OBJECT (timeline, "Track element %s inpoint would be negative,"
|
|
" not trimming", GES_TIMELINE_ELEMENT_NAME (track_element));
|
|
gst_object_unref (toplevel);
|
|
return FALSE;
|
|
}
|
|
|
|
inpoint = inpoint + position - start;
|
|
real_dur = _END (element) - position;
|
|
duration = CLAMP (real_dur, 0, max_duration > inpoint ?
|
|
max_duration - inpoint : G_MAXUINT64);
|
|
|
|
|
|
/* If we already are at max duration or duration == 0 do no useless work */
|
|
if ((duration == _DURATION (track_element) &&
|
|
_DURATION (track_element) == _MAXDURATION (track_element)) ||
|
|
(duration == 0 && _DURATION (element) == 0)) {
|
|
GST_DEBUG_OBJECT (track_element,
|
|
"Duration already == max_duration, no triming");
|
|
gst_object_unref (toplevel);
|
|
return FALSE;
|
|
}
|
|
|
|
timeline->priv->needs_transitions_update = FALSE;
|
|
_set_start0 (GES_TIMELINE_ELEMENT (track_element), position);
|
|
_set_inpoint0 (GES_TIMELINE_ELEMENT (track_element), inpoint);
|
|
timeline->priv->needs_transitions_update = TRUE;
|
|
|
|
_set_duration0 (GES_TIMELINE_ELEMENT (track_element), duration);
|
|
if (GES_IS_GROUP (toplevel))
|
|
GES_CONTAINER (toplevel)->children_control_mode = old_mode;
|
|
|
|
gst_object_unref (toplevel);
|
|
break;
|
|
}
|
|
case GES_EDGE_END:
|
|
{
|
|
cur = g_hash_table_lookup (timeline->priv->by_end, track_element);
|
|
snapped = ges_timeline_snap_position (timeline, track_element, cur,
|
|
position, TRUE);
|
|
if (snapped)
|
|
position = *snapped;
|
|
|
|
/* Calculate new values */
|
|
real_dur = position - start;
|
|
duration = MAX (0, real_dur);
|
|
duration = MIN (duration, max_duration - _INPOINT (track_element));
|
|
|
|
/* Not moving, avoid overhead */
|
|
if (duration == _DURATION (track_element)) {
|
|
GST_DEBUG_OBJECT (track_element, "No change in duration");
|
|
return FALSE;
|
|
}
|
|
|
|
_set_duration0 (GES_TIMELINE_ELEMENT (track_element), duration);
|
|
break;
|
|
}
|
|
default:
|
|
GST_WARNING ("Can not trim with %i GESEdge", edge);
|
|
return FALSE;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
timeline_ripple_object (GESTimeline * timeline, GESTrackElement * obj,
|
|
GList * layers, GESEdge edge, guint64 position)
|
|
{
|
|
GList *tmp, *moved_clips = NULL;
|
|
GESTrackElement *trackelement;
|
|
GESContainer *container;
|
|
guint64 duration, new_start, *snapped, *cur;
|
|
gint64 offset;
|
|
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
|
|
mv_ctx->ignore_needs_ctx = TRUE;
|
|
timeline->priv->needs_rollback = FALSE;
|
|
if (!ges_timeline_set_moving_context (timeline, obj, GES_EDIT_MODE_RIPPLE,
|
|
edge, layers))
|
|
goto error;
|
|
|
|
switch (edge) {
|
|
case GES_EDGE_NONE:
|
|
GST_DEBUG ("Simply rippling");
|
|
|
|
/* We should be smart here to avoid recalculate transitions when possible */
|
|
cur = g_hash_table_lookup (timeline->priv->by_end, obj);
|
|
snapped = ges_timeline_snap_position (timeline, obj, cur, position, TRUE);
|
|
if (snapped)
|
|
position = *snapped;
|
|
|
|
offset = position - _START (obj);
|
|
|
|
for (tmp = mv_ctx->moving_trackelements; tmp; tmp = tmp->next) {
|
|
trackelement = GES_TRACK_ELEMENT (tmp->data);
|
|
new_start = _START (trackelement) + offset;
|
|
|
|
container = add_toplevel_container (mv_ctx, trackelement);
|
|
/* Make sure not to move 2 times the same Clip */
|
|
if (g_list_find (moved_clips, container) == NULL) {
|
|
_set_start0 (GES_TIMELINE_ELEMENT (trackelement), new_start);
|
|
moved_clips = g_list_prepend (moved_clips, container);
|
|
}
|
|
|
|
}
|
|
g_list_free (moved_clips);
|
|
_set_start0 (GES_TIMELINE_ELEMENT (obj), position);
|
|
|
|
if (timeline->priv->needs_rollback && !timeline->priv->rolling_back) {
|
|
timeline->priv->rolling_back = TRUE;
|
|
for (tmp = mv_ctx->moving_trackelements; tmp; tmp = tmp->next) {
|
|
trackelement = GES_TRACK_ELEMENT (tmp->data);
|
|
new_start = _START (trackelement) - offset;
|
|
|
|
container = add_toplevel_container (mv_ctx, trackelement);
|
|
/* Make sure not to move 2 times the same Clip */
|
|
if (g_list_find (moved_clips, container) == NULL) {
|
|
_set_start0 (GES_TIMELINE_ELEMENT (trackelement), new_start);
|
|
moved_clips = g_list_prepend (moved_clips, container);
|
|
}
|
|
|
|
}
|
|
g_list_free (moved_clips);
|
|
_set_start0 (GES_TIMELINE_ELEMENT (obj), position - offset);
|
|
|
|
ges_timeline_emit_snappig (timeline, obj, NULL);
|
|
mv_ctx->needs_move_ctx = TRUE;
|
|
timeline->priv->rolling_back = FALSE;
|
|
|
|
goto error;
|
|
}
|
|
|
|
break;
|
|
case GES_EDGE_END:
|
|
timeline->priv->needs_transitions_update = FALSE;
|
|
GST_DEBUG ("Rippling end");
|
|
|
|
cur = g_hash_table_lookup (timeline->priv->by_end, obj);
|
|
snapped = ges_timeline_snap_position (timeline, obj, cur, position, TRUE);
|
|
if (snapped)
|
|
position = *snapped;
|
|
|
|
duration = _DURATION (obj);
|
|
|
|
if (!ges_timeline_trim_object_simple (timeline,
|
|
GES_TIMELINE_ELEMENT (obj), NULL, GES_EDGE_END, position,
|
|
FALSE)) {
|
|
return FALSE;
|
|
}
|
|
|
|
offset = _DURATION (obj) - duration;
|
|
for (tmp = mv_ctx->moving_trackelements; tmp; tmp = tmp->next) {
|
|
trackelement = GES_TRACK_ELEMENT (tmp->data);
|
|
new_start = _START (trackelement) + offset;
|
|
|
|
container = add_toplevel_container (mv_ctx, trackelement);
|
|
if (GES_IS_GROUP (container))
|
|
container->children_control_mode = GES_CHILDREN_UPDATE_OFFSETS;
|
|
/* Make sure not to move 2 times the same Clip */
|
|
if (g_list_find (moved_clips, container) == NULL) {
|
|
_set_start0 (GES_TIMELINE_ELEMENT (trackelement), new_start);
|
|
moved_clips = g_list_prepend (moved_clips, container);
|
|
}
|
|
if (GES_IS_GROUP (container))
|
|
container->children_control_mode = GES_CHILDREN_UPDATE;
|
|
}
|
|
|
|
g_list_free (moved_clips);
|
|
timeline->priv->needs_transitions_update = TRUE;
|
|
GST_DEBUG ("Done Rippling end");
|
|
break;
|
|
case GES_EDGE_START:
|
|
GST_INFO ("Ripple start doesn't make sense, trimming instead");
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
timeline_trim_object (timeline, obj, layers, edge, position);
|
|
break;
|
|
default:
|
|
GST_DEBUG ("Can not ripple edge: %i", edge);
|
|
|
|
break;
|
|
}
|
|
|
|
mv_ctx->ignore_needs_ctx = FALSE;
|
|
|
|
return TRUE;
|
|
|
|
error:
|
|
mv_ctx->ignore_needs_ctx = FALSE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
timeline_slide_object (GESTimeline * timeline, GESTrackElement * obj,
|
|
GList * layers, GESEdge edge, guint64 position)
|
|
{
|
|
|
|
/* FIXME implement me! */
|
|
GST_FIXME_OBJECT (timeline, "Slide mode editing not implemented yet");
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
timeline_trim_object (GESTimeline * timeline, GESTrackElement * object,
|
|
GList * layers, GESEdge edge, guint64 position)
|
|
{
|
|
gboolean ret = FALSE;
|
|
GstClockTime cpos;
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
|
|
mv_ctx->ignore_needs_ctx = TRUE;
|
|
|
|
timeline->priv->needs_rollback = FALSE;
|
|
if (!ges_timeline_set_moving_context (timeline, object, GES_EDIT_MODE_TRIM,
|
|
edge, layers))
|
|
goto end;
|
|
|
|
switch (edge) {
|
|
case GES_EDGE_START:
|
|
cpos = GES_TIMELINE_ELEMENT_START (object);
|
|
break;
|
|
case GES_EDGE_END:
|
|
cpos = GES_TIMELINE_ELEMENT_END (object);
|
|
break;
|
|
default:
|
|
goto end;
|
|
}
|
|
ret = ges_timeline_trim_object_simple (timeline,
|
|
GES_TIMELINE_ELEMENT (object), layers, edge, position, TRUE);
|
|
|
|
if (timeline->priv->needs_rollback && !timeline->priv->rolling_back) {
|
|
timeline->priv->rolling_back = TRUE;
|
|
ret = FALSE;
|
|
timeline_trim_object (timeline, object, layers, edge, cpos);
|
|
ges_timeline_emit_snappig (timeline, object, NULL);
|
|
timeline->priv->rolling_back = FALSE;
|
|
}
|
|
|
|
end:
|
|
mv_ctx->ignore_needs_ctx = FALSE;
|
|
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
timeline_roll_object (GESTimeline * timeline, GESTrackElement * obj,
|
|
GList * layers, GESEdge edge, guint64 position)
|
|
{
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
guint64 start, duration, end, tmpstart, tmpduration, tmpend, *snapped, *cur;
|
|
gboolean ret = TRUE;
|
|
GList *tmp;
|
|
|
|
mv_ctx->ignore_needs_ctx = TRUE;
|
|
|
|
GST_DEBUG_OBJECT (obj, "Rolling object to %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (position));
|
|
|
|
if (!ges_timeline_set_moving_context (timeline, obj, GES_EDIT_MODE_ROLL,
|
|
edge, layers))
|
|
goto error;
|
|
|
|
start = _START (obj);
|
|
duration = _DURATION (obj);
|
|
end = start + duration;
|
|
|
|
timeline->priv->needs_transitions_update = FALSE;
|
|
switch (edge) {
|
|
case GES_EDGE_START:
|
|
|
|
/* Avoid negative durations */
|
|
if (position < mv_ctx->max_trim_pos || position > end ||
|
|
position < mv_ctx->min_trim_pos)
|
|
goto error;
|
|
|
|
cur = g_hash_table_lookup (timeline->priv->by_start, obj);
|
|
snapped = ges_timeline_snap_position (timeline, obj, cur, position, TRUE);
|
|
if (snapped)
|
|
position = *snapped;
|
|
|
|
ret &= ges_timeline_trim_object_simple (timeline,
|
|
GES_TIMELINE_ELEMENT (obj), layers, GES_EDGE_START, position, FALSE);
|
|
|
|
if (!ret) {
|
|
GST_INFO_OBJECT (timeline, "Could not trim %s",
|
|
GES_TIMELINE_ELEMENT_NAME (obj));
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
/* In the case we reached max_duration we just make sure to roll
|
|
* everything to the real new position */
|
|
position = _START (obj);
|
|
|
|
/* Send back changes to the neighbourhood */
|
|
for (tmp = mv_ctx->moving_trackelements; tmp; tmp = tmp->next) {
|
|
GESTimelineElement *tmpelement = GES_TIMELINE_ELEMENT (tmp->data);
|
|
|
|
tmpstart = _START (tmpelement);
|
|
tmpduration = _DURATION (tmpelement);
|
|
tmpend = tmpstart + tmpduration;
|
|
|
|
/* Check that the object should be resized at this position
|
|
* even if an error accurs, we keep doing our job */
|
|
if (tmpend == start) {
|
|
ret &= ges_timeline_trim_object_simple (timeline, tmpelement, NULL,
|
|
GES_EDGE_END, position, FALSE);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case GES_EDGE_END:
|
|
|
|
/* Avoid negative durations */
|
|
if (position > mv_ctx->max_trim_pos || position < start)
|
|
goto error;
|
|
|
|
end = _START (obj) + _DURATION (obj);
|
|
|
|
cur = g_hash_table_lookup (timeline->priv->by_end, obj);
|
|
snapped = ges_timeline_snap_position (timeline, obj, cur, position, TRUE);
|
|
if (snapped)
|
|
position = *snapped;
|
|
|
|
ret &= ges_timeline_trim_object_simple (timeline,
|
|
GES_TIMELINE_ELEMENT (obj), NULL, GES_EDGE_END, position, FALSE);
|
|
|
|
if (ret == FALSE) {
|
|
GST_DEBUG_OBJECT (timeline, "No triming, bailing out");
|
|
goto done;
|
|
}
|
|
|
|
/* In the case we reached max_duration we just make sure to roll
|
|
* everything to the real new position */
|
|
position = _START (obj) + _DURATION (obj);
|
|
|
|
/* Send back changes to the neighbourhood */
|
|
for (tmp = mv_ctx->moving_trackelements; tmp; tmp = tmp->next) {
|
|
GESTimelineElement *tmpelement = GES_TIMELINE_ELEMENT (tmp->data);
|
|
|
|
tmpstart = _START (tmpelement);
|
|
tmpduration = _DURATION (tmpelement);
|
|
tmpend = tmpstart + tmpduration;
|
|
|
|
/* Check that the object should be resized at this position
|
|
* even if an error accure, we keep doing our job */
|
|
if (end == tmpstart) {
|
|
ret &= ges_timeline_trim_object_simple (timeline, tmpelement, NULL,
|
|
GES_EDGE_START, position, FALSE);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
GST_DEBUG ("Edge type %i not handled here", edge);
|
|
break;
|
|
}
|
|
|
|
done:
|
|
timeline->priv->needs_transitions_update = TRUE;
|
|
mv_ctx->ignore_needs_ctx = FALSE;
|
|
|
|
return ret;
|
|
|
|
error:
|
|
GST_DEBUG_OBJECT (obj, "Could not roll edge %d to %" GST_TIME_FORMAT,
|
|
edge, GST_TIME_ARGS (position));
|
|
|
|
ret = FALSE;
|
|
goto done;
|
|
}
|
|
|
|
gboolean
|
|
timeline_move_object (GESTimeline * timeline, GESTrackElement * object,
|
|
GList * layers, GESEdge edge, guint64 position)
|
|
{
|
|
if (!ges_timeline_set_moving_context (timeline, object, GES_EDIT_MODE_NORMAL,
|
|
edge, layers)) {
|
|
GST_DEBUG_OBJECT (object, "Could not move to %" GST_TIME_FORMAT,
|
|
GST_TIME_ARGS (position));
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
return ges_timeline_move_object_simple (timeline,
|
|
GES_TIMELINE_ELEMENT (object), layers, edge, position);
|
|
}
|
|
|
|
gboolean
|
|
ges_timeline_move_object_simple (GESTimeline * timeline,
|
|
GESTimelineElement * element, GList * layers, GESEdge edge,
|
|
guint64 position)
|
|
{
|
|
GstClockTime cpos = GES_TIMELINE_ELEMENT_START (element);
|
|
guint64 *snap_end, *snap_st, *cur, off1, off2, end;
|
|
GESTrackElement *track_element;
|
|
|
|
/* We only work with GESSource-s and we check that we are not already moving
|
|
* element ourself*/
|
|
if (GES_IS_SOURCE (element) == FALSE ||
|
|
g_list_find (timeline->priv->movecontext.moving_trackelements, element))
|
|
return FALSE;
|
|
|
|
timeline->priv->needs_rollback = FALSE;
|
|
track_element = GES_TRACK_ELEMENT (element);
|
|
end = position + _DURATION (get_toplevel_container (track_element));
|
|
cur = g_hash_table_lookup (timeline->priv->by_end, track_element);
|
|
|
|
GST_DEBUG_OBJECT (timeline, "Moving %" GST_PTR_FORMAT "to %"
|
|
GST_TIME_FORMAT " (end %" GST_TIME_FORMAT ")", element,
|
|
GST_TIME_ARGS (position), GST_TIME_ARGS (end));
|
|
|
|
snap_end = ges_timeline_snap_position (timeline, track_element, cur, end,
|
|
FALSE);
|
|
if (snap_end)
|
|
off1 = end > *snap_end ? end - *snap_end : *snap_end - end;
|
|
else
|
|
off1 = G_MAXUINT64;
|
|
|
|
cur = g_hash_table_lookup (timeline->priv->by_start, track_element);
|
|
snap_st =
|
|
ges_timeline_snap_position (timeline, track_element, cur, position,
|
|
FALSE);
|
|
if (snap_st)
|
|
off2 = position > *snap_st ? position - *snap_st : *snap_st - position;
|
|
else
|
|
off2 = G_MAXUINT64;
|
|
|
|
/* In the case we could snap on both sides, we snap on the end */
|
|
if (snap_end && off1 <= off2) {
|
|
position = position + *snap_end - end;
|
|
ges_timeline_emit_snappig (timeline, track_element, snap_end);
|
|
} else if (snap_st) {
|
|
position = position + *snap_st - position;
|
|
ges_timeline_emit_snappig (timeline, track_element, snap_st);
|
|
} else
|
|
ges_timeline_emit_snappig (timeline, track_element, NULL);
|
|
timeline->priv->needs_rollback = FALSE;
|
|
|
|
_set_start0 (GES_TIMELINE_ELEMENT (track_element), position);
|
|
|
|
if (timeline->priv->needs_rollback && !timeline->priv->rolling_back) {
|
|
timeline->priv->needs_rollback = FALSE;
|
|
timeline->priv->rolling_back = TRUE;
|
|
ges_timeline_move_object_simple (timeline, element, layers, edge, cpos);
|
|
ges_timeline_emit_snappig (timeline, track_element, NULL);
|
|
timeline->priv->rolling_back = FALSE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
timeline_context_to_layer (GESTimeline * timeline, gint offset)
|
|
{
|
|
gboolean ret = TRUE;
|
|
MoveContext *mv_ctx = &timeline->priv->movecontext;
|
|
|
|
/* Layer's priority is always positive */
|
|
if (offset != 0 && (offset > 0 || mv_ctx->min_move_layer >= -offset)) {
|
|
GHashTableIter iter;
|
|
GESContainer *key, *value;
|
|
GESLayer *new_layer;
|
|
guint prio;
|
|
|
|
mv_ctx->ignore_needs_ctx = TRUE;
|
|
|
|
GST_DEBUG ("Moving %d object, offset %d",
|
|
g_hash_table_size (mv_ctx->toplevel_containers), offset);
|
|
|
|
timeline->priv->needs_rollback = FALSE;
|
|
g_hash_table_iter_init (&iter, mv_ctx->toplevel_containers);
|
|
while (g_hash_table_iter_next (&iter, (gpointer *) & key,
|
|
(gpointer *) & value)) {
|
|
|
|
if (GES_IS_CLIP (value)) {
|
|
prio = ges_clip_get_layer_priority (GES_CLIP (value));
|
|
|
|
/* We know that the layer exists as we created it */
|
|
new_layer =
|
|
GES_LAYER (g_list_nth_data (timeline->layers, prio + offset));
|
|
|
|
if (new_layer == NULL) {
|
|
do {
|
|
new_layer = ges_timeline_append_layer (timeline);
|
|
} while (ges_layer_get_priority (new_layer) < prio + offset);
|
|
}
|
|
|
|
ret &= ges_clip_move_to_layer (GES_CLIP (key), new_layer);
|
|
} else if (GES_IS_GROUP (value)) {
|
|
guint32 last_prio = _PRIORITY (value) + offset +
|
|
GES_CONTAINER_HEIGHT (value) - 1;
|
|
|
|
new_layer = GES_LAYER (g_list_nth_data (timeline->layers, last_prio));
|
|
|
|
if (new_layer == NULL) {
|
|
do {
|
|
new_layer = ges_timeline_append_layer (timeline);
|
|
} while (ges_layer_get_priority (new_layer) < last_prio);
|
|
}
|
|
|
|
_set_priority0 (GES_TIMELINE_ELEMENT (value),
|
|
_PRIORITY (value) + offset);
|
|
}
|
|
}
|
|
|
|
/* Readjust min_move_layer */
|
|
mv_ctx->min_move_layer = mv_ctx->min_move_layer + offset;
|
|
|
|
mv_ctx->ignore_needs_ctx = FALSE;
|
|
|
|
if (timeline->priv->needs_rollback && !timeline->priv->rolling_back) {
|
|
ret = FALSE;
|
|
timeline->priv->rolling_back = TRUE;
|
|
timeline_context_to_layer (timeline, -offset);
|
|
timeline->priv->rolling_back = FALSE;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void
|
|
timeline_add_group (GESTimeline * timeline, GESGroup * group)
|
|
{
|
|
GST_DEBUG_OBJECT (timeline, "Adding group %" GST_PTR_FORMAT, group);
|
|
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
timeline->priv->groups = g_list_prepend (timeline->priv->groups,
|
|
gst_object_ref_sink (group));
|
|
|
|
ges_timeline_element_set_timeline (GES_TIMELINE_ELEMENT (group), timeline);
|
|
}
|
|
|
|
void
|
|
timeline_remove_group (GESTimeline * timeline, GESGroup * group)
|
|
{
|
|
GST_DEBUG_OBJECT (timeline, "Removing group %" GST_PTR_FORMAT, group);
|
|
|
|
timeline->priv->groups = g_list_remove (timeline->priv->groups, group);
|
|
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
ges_timeline_element_set_timeline (GES_TIMELINE_ELEMENT (group), NULL);
|
|
gst_object_unref (group);
|
|
}
|
|
|
|
GList *
|
|
timeline_get_groups (GESTimeline * timeline)
|
|
{
|
|
return timeline->priv->groups;
|
|
}
|
|
|
|
static GPtrArray *
|
|
select_tracks_for_object_default (GESTimeline * timeline,
|
|
GESClip * clip, GESTrackElement * tr_object, gpointer user_data)
|
|
{
|
|
GPtrArray *result;
|
|
GList *tmp;
|
|
|
|
result = g_ptr_array_new ();
|
|
|
|
for (tmp = timeline->tracks; tmp; tmp = tmp->next) {
|
|
GESTrack *track = GES_TRACK (tmp->data);
|
|
|
|
if ((track->type & ges_track_element_get_track_type (tr_object))) {
|
|
gst_object_ref (track);
|
|
g_ptr_array_add (result, track);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
add_object_to_tracks (GESTimeline * timeline, GESClip * clip, GESTrack * track)
|
|
{
|
|
gint i;
|
|
GList *tmp, *list;
|
|
GESTrackType types, visited_type = GES_TRACK_TYPE_UNKNOWN;
|
|
|
|
GST_DEBUG_OBJECT (timeline, "Creating %" GST_PTR_FORMAT
|
|
" trackelements and adding them to our tracks", clip);
|
|
|
|
types = ges_clip_get_supported_formats (clip);
|
|
if (track) {
|
|
if ((types & track->type) == 0)
|
|
return;
|
|
types = track->type;
|
|
}
|
|
|
|
for (i = 0, tmp = timeline->tracks; tmp; tmp = tmp->next, i++) {
|
|
GESTrack *track = GES_TRACK (tmp->data);
|
|
|
|
if (((track->type & types) == 0 || (track->type & visited_type)))
|
|
continue;
|
|
|
|
list = ges_clip_create_track_elements (clip, track->type);
|
|
g_list_free (list);
|
|
}
|
|
}
|
|
|
|
static void
|
|
layer_auto_transition_changed_cb (GESLayer * layer,
|
|
GParamSpec * arg G_GNUC_UNUSED, GESTimeline * timeline)
|
|
{
|
|
GList *tmp, *clips;
|
|
|
|
timeline->priv->needs_rollback = FALSE;
|
|
_create_transitions_on_layer (timeline, layer, NULL, NULL,
|
|
_create_auto_transition_from_transitions);
|
|
|
|
clips = ges_layer_get_clips (layer);
|
|
for (tmp = clips; tmp; tmp = tmp->next) {
|
|
if (GES_IS_TRANSITION_CLIP (tmp->data)) {
|
|
GList *tmpautotrans;
|
|
gboolean found = FALSE;
|
|
|
|
for (tmpautotrans = timeline->priv->auto_transitions; tmpautotrans;
|
|
tmpautotrans = tmpautotrans->next) {
|
|
if (GES_AUTO_TRANSITION (tmpautotrans->data)->transition_clip ==
|
|
tmp->data) {
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
GST_ERROR_OBJECT (timeline,
|
|
"Transition %s could not be wrapped into an auto transition"
|
|
" REMOVING it", GES_TIMELINE_ELEMENT_NAME (tmp->data));
|
|
|
|
ges_layer_remove_clip (layer, tmp->data);
|
|
}
|
|
}
|
|
}
|
|
g_list_free_full (clips, gst_object_unref);
|
|
|
|
if (timeline->priv->needs_rollback) {
|
|
GList *tmp, *trans;
|
|
|
|
ges_layer_set_auto_transition (layer, FALSE);
|
|
GST_ERROR_OBJECT (layer, "Has overlapping transition, "
|
|
" we can't handle that, setting auto_transition"
|
|
" to FALSE, and removing all transitions");
|
|
trans = g_list_copy (timeline->priv->auto_transitions);
|
|
for (tmp = trans; tmp; tmp = tmp->next) {
|
|
g_signal_emit_by_name (tmp->data, "destroy-me");
|
|
}
|
|
|
|
g_list_free (trans);
|
|
|
|
trans = ges_layer_get_clips (layer);
|
|
for (tmp = trans; tmp; tmp = tmp->next) {
|
|
if (GES_IS_TRANSITION_CLIP (tmp->data))
|
|
ges_layer_remove_clip (layer, tmp->data);
|
|
}
|
|
g_list_free_full (trans, gst_object_unref);
|
|
}
|
|
}
|
|
|
|
static void
|
|
clip_track_element_added_cb (GESClip * clip,
|
|
GESTrackElement * track_element, GESTimeline * timeline)
|
|
{
|
|
guint i;
|
|
GESTrack *track;
|
|
gboolean is_source;
|
|
GPtrArray *tracks = NULL;
|
|
GESTrackElement *existing_src = NULL;
|
|
|
|
if (timeline->priv->ignore_track_element_added == clip) {
|
|
GST_DEBUG_OBJECT (timeline, "Ignoring element added (%" GST_PTR_FORMAT
|
|
" in %" GST_PTR_FORMAT, track_element, clip);
|
|
|
|
return;
|
|
}
|
|
|
|
if (ges_track_element_get_track (track_element)) {
|
|
GST_WARNING_OBJECT (track_element, "Already in a track");
|
|
|
|
return;
|
|
}
|
|
|
|
g_signal_emit (G_OBJECT (timeline),
|
|
ges_timeline_signals[SELECT_TRACKS_FOR_OBJECT], 0, clip, track_element,
|
|
&tracks);
|
|
|
|
if (!tracks || tracks->len == 0) {
|
|
GST_WARNING_OBJECT (timeline, "Got no Track to add %p (type %s), removing"
|
|
" from clip (stopping 'child-added' signal emission).",
|
|
track_element, ges_track_type_name (ges_track_element_get_track_type
|
|
(track_element)));
|
|
|
|
if (tracks)
|
|
g_ptr_array_unref (tracks);
|
|
|
|
g_signal_stop_emission_by_name (clip, "child-added");
|
|
ges_container_remove (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element));
|
|
|
|
return;
|
|
}
|
|
|
|
/* We add the current element to the first track */
|
|
track = g_ptr_array_index (tracks, 0);
|
|
|
|
is_source = g_type_is_a (G_OBJECT_TYPE (track_element), GES_TYPE_SOURCE);
|
|
if (is_source)
|
|
existing_src = ges_clip_find_track_element (clip, track, GES_TYPE_SOURCE);
|
|
|
|
if (existing_src == NULL) {
|
|
if (!ges_track_add_element (track, track_element)) {
|
|
GST_WARNING_OBJECT (clip, "Failed to add track element to track");
|
|
ges_container_remove (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element));
|
|
g_ptr_array_unref (tracks);
|
|
return;
|
|
}
|
|
} else {
|
|
GST_INFO_OBJECT (clip, "Already had a Source Element in %" GST_PTR_FORMAT
|
|
" of type %s, removing new one. (stopping 'child-added' emission)",
|
|
track, G_OBJECT_TYPE_NAME (track_element));
|
|
g_signal_stop_emission_by_name (clip, "child-added");
|
|
ges_container_remove (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element));
|
|
}
|
|
gst_object_unref (track);
|
|
g_clear_object (&existing_src);
|
|
|
|
/* And create copies to add to other tracks */
|
|
timeline->priv->ignore_track_element_added = clip;
|
|
for (i = 1; i < tracks->len; i++) {
|
|
GESTrack *track;
|
|
GESTrackElement *track_element_copy;
|
|
|
|
track = g_ptr_array_index (tracks, i);
|
|
if (is_source)
|
|
existing_src = ges_clip_find_track_element (clip, track, GES_TYPE_SOURCE);
|
|
if (existing_src == NULL) {
|
|
ges_container_remove (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element));
|
|
gst_object_unref (track);
|
|
g_ptr_array_unref (tracks);
|
|
continue;
|
|
} else {
|
|
GST_INFO_OBJECT (clip, "Already had a Source Element in %" GST_PTR_FORMAT
|
|
" of type %s, removing new one. (stopping 'child-added' emission)",
|
|
track, G_OBJECT_TYPE_NAME (track_element));
|
|
g_signal_stop_emission_by_name (clip, "child-added");
|
|
ges_container_remove (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element));
|
|
}
|
|
g_clear_object (&existing_src);
|
|
|
|
track_element_copy =
|
|
GES_TRACK_ELEMENT (ges_timeline_element_copy (GES_TIMELINE_ELEMENT
|
|
(track_element), TRUE));
|
|
|
|
GST_LOG_OBJECT (timeline, "Trying to add %p to track %p",
|
|
track_element_copy, track);
|
|
|
|
if (!ges_container_add (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element_copy))) {
|
|
GST_WARNING_OBJECT (clip, "Failed to add track element to clip");
|
|
gst_object_unref (track_element_copy);
|
|
g_ptr_array_unref (tracks);
|
|
return;
|
|
}
|
|
|
|
if (!ges_track_add_element (track, track_element_copy)) {
|
|
GST_WARNING_OBJECT (clip, "Failed to add track element to track");
|
|
ges_container_remove (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element_copy));
|
|
gst_object_unref (track_element_copy);
|
|
g_ptr_array_unref (tracks);
|
|
return;
|
|
}
|
|
|
|
gst_object_unref (track);
|
|
}
|
|
timeline->priv->ignore_track_element_added = NULL;
|
|
g_ptr_array_unref (tracks);
|
|
}
|
|
|
|
static void
|
|
clip_track_element_removed_cb (GESClip * clip,
|
|
GESTrackElement * track_element, GESTimeline * timeline)
|
|
{
|
|
GESTrack *track = ges_track_element_get_track (track_element);
|
|
|
|
if (track)
|
|
ges_track_remove_element (track, track_element);
|
|
}
|
|
|
|
static void
|
|
layer_object_added_cb (GESLayer * layer, GESClip * clip, GESTimeline * timeline)
|
|
{
|
|
GESProject *project;
|
|
|
|
/* We make sure not to be connected twice */
|
|
g_signal_handlers_disconnect_by_func (clip, clip_track_element_added_cb,
|
|
timeline);
|
|
g_signal_handlers_disconnect_by_func (clip, clip_track_element_removed_cb,
|
|
timeline);
|
|
|
|
/* And we connect to the object */
|
|
g_signal_connect (clip, "child-added",
|
|
G_CALLBACK (clip_track_element_added_cb), timeline);
|
|
g_signal_connect (clip, "child-removed",
|
|
G_CALLBACK (clip_track_element_removed_cb), timeline);
|
|
|
|
if (ges_clip_is_moving_from_layer (clip)) {
|
|
GST_DEBUG ("Clip %p moving from one layer to another, not creating "
|
|
"TrackElement", clip);
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
_create_transitions_on_layer (timeline, layer, NULL, NULL,
|
|
_find_transition_from_auto_transitions);
|
|
return;
|
|
}
|
|
|
|
|
|
add_object_to_tracks (timeline, clip, NULL);
|
|
|
|
GST_DEBUG ("Making sure that the asset is in our project");
|
|
project =
|
|
GES_PROJECT (ges_extractable_get_asset (GES_EXTRACTABLE (timeline)));
|
|
ges_project_add_asset (project,
|
|
ges_extractable_get_asset (GES_EXTRACTABLE (clip)));
|
|
|
|
GST_DEBUG ("Done");
|
|
}
|
|
|
|
static void
|
|
layer_priority_changed_cb (GESLayer * layer,
|
|
GParamSpec * arg G_GNUC_UNUSED, GESTimeline * timeline)
|
|
{
|
|
timeline->layers = g_list_sort (timeline->layers, (GCompareFunc)
|
|
sort_layers);
|
|
}
|
|
|
|
static void
|
|
layer_object_removed_cb (GESLayer * layer, GESClip * clip,
|
|
GESTimeline * timeline)
|
|
{
|
|
GList *trackelements, *tmp;
|
|
|
|
if (ges_clip_is_moving_from_layer (clip)) {
|
|
GST_DEBUG ("Clip %p is moving from a layer to another, not doing"
|
|
" anything on it", clip);
|
|
return;
|
|
}
|
|
|
|
GST_DEBUG ("Clip %p removed from layer %p", clip, layer);
|
|
|
|
/* Go over the clip's track element and figure out which one belongs to
|
|
* the list of tracks we control */
|
|
|
|
trackelements = ges_container_get_children (GES_CONTAINER (clip), FALSE);
|
|
for (tmp = trackelements; tmp; tmp = tmp->next) {
|
|
GESTrackElement *track_element = (GESTrackElement *) tmp->data;
|
|
GESTrack *track = ges_track_element_get_track (track_element);
|
|
|
|
GST_DEBUG_OBJECT (timeline, "Trying to remove TrackElement %p",
|
|
track_element);
|
|
|
|
/* FIXME Check if we should actually check that we control the
|
|
* track in the new management of TrackElement context */
|
|
LOCK_DYN (timeline);
|
|
if (G_LIKELY (g_list_find_custom (timeline->priv->priv_tracks, track,
|
|
(GCompareFunc) custom_find_track) || track == NULL)) {
|
|
GST_DEBUG ("Belongs to one of the tracks we control");
|
|
|
|
ges_container_remove (GES_CONTAINER (clip),
|
|
GES_TIMELINE_ELEMENT (track_element));
|
|
}
|
|
UNLOCK_DYN (timeline);
|
|
}
|
|
g_signal_handlers_disconnect_by_func (clip, clip_track_element_added_cb,
|
|
timeline);
|
|
g_signal_handlers_disconnect_by_func (clip, clip_track_element_removed_cb,
|
|
timeline);
|
|
|
|
g_list_free_full (trackelements, gst_object_unref);
|
|
|
|
GST_DEBUG ("Done");
|
|
}
|
|
|
|
static void
|
|
trackelement_start_changed_cb (GESTrackElement * child,
|
|
GParamSpec * arg G_GNUC_UNUSED, GESTimeline * timeline)
|
|
{
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
TrackObjIters *iters = g_hash_table_lookup (priv->obj_iters, child);
|
|
|
|
if (G_LIKELY (iters->iter_by_layer))
|
|
g_sequence_sort_changed (iters->iter_by_layer,
|
|
(GCompareDataFunc) element_start_compare, NULL);
|
|
|
|
if (GES_IS_SOURCE (child)) {
|
|
sort_track_elements (timeline, iters);
|
|
sort_starts_ends_start (timeline, iters);
|
|
sort_starts_ends_end (timeline, iters);
|
|
|
|
/* If the timeline is set to snap objects together, we
|
|
* are sure that all movement of TrackElement-s are done within
|
|
* the moving context, so we do not need to recalculate the
|
|
* move context as often */
|
|
if (timeline->priv->movecontext.ignore_needs_ctx &&
|
|
timeline->priv->snapping_distance == 0)
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
|
|
create_transitions (timeline, child);
|
|
}
|
|
}
|
|
|
|
static void
|
|
trackelement_priority_changed_cb (GESTrackElement * child,
|
|
GParamSpec * arg G_GNUC_UNUSED, GESTimeline * timeline)
|
|
{
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
|
|
GList *layer_node = g_list_find_custom (timeline->layers,
|
|
GINT_TO_POINTER (_ges_track_element_get_layer_priority (child)),
|
|
(GCompareFunc) find_layer_by_prio);
|
|
GESLayer *layer = layer_node ? layer_node->data : NULL;
|
|
TrackObjIters *iters = g_hash_table_lookup (priv->obj_iters,
|
|
child);
|
|
|
|
if (G_UNLIKELY (layer == NULL)) {
|
|
GST_ERROR_OBJECT (timeline,
|
|
"Changing a TrackElement prio, which would not "
|
|
"land in no layer we are controlling");
|
|
if (iters->iter_by_layer)
|
|
g_sequence_remove (iters->iter_by_layer);
|
|
iters->iter_by_layer = NULL;
|
|
iters->layer = NULL;
|
|
} else {
|
|
/* If it moves from layer, properly change it */
|
|
if (layer != iters->layer) {
|
|
GSequence *by_layer_sequence =
|
|
g_hash_table_lookup (priv->by_layer, layer);
|
|
|
|
GST_DEBUG_OBJECT (child, "Moved from layer %" GST_PTR_FORMAT
|
|
"(prio %d) to" " %" GST_PTR_FORMAT " (prio %d)", layer,
|
|
ges_layer_get_priority (layer), iters->layer,
|
|
ges_layer_get_priority (iters->layer));
|
|
|
|
g_sequence_remove (iters->iter_by_layer);
|
|
iters->iter_by_layer =
|
|
g_sequence_insert_sorted (by_layer_sequence, child,
|
|
(GCompareDataFunc) element_start_compare, NULL);
|
|
iters->layer = layer;
|
|
} else {
|
|
g_sequence_sort_changed (iters->iter_by_layer,
|
|
(GCompareDataFunc) element_start_compare, NULL);
|
|
}
|
|
}
|
|
|
|
if (GES_IS_SOURCE (child))
|
|
sort_track_elements (timeline, iters);
|
|
}
|
|
|
|
static void
|
|
trackelement_duration_changed_cb (GESTrackElement * child,
|
|
GParamSpec * arg G_GNUC_UNUSED, GESTimeline * timeline)
|
|
{
|
|
GESTimelinePrivate *priv = timeline->priv;
|
|
TrackObjIters *iters = g_hash_table_lookup (priv->obj_iters, child);
|
|
|
|
if (GES_IS_SOURCE (child)) {
|
|
sort_starts_ends_end (timeline, iters);
|
|
|
|
/* If the timeline is set to snap objects together, we
|
|
* are sure that all movement of TrackElement-s are done within
|
|
* the moving context, so we do not need to recalculate the
|
|
* move context as often */
|
|
if (timeline->priv->movecontext.ignore_needs_ctx &&
|
|
timeline->priv->snapping_distance == 0) {
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
}
|
|
|
|
create_transitions (timeline, child);
|
|
}
|
|
}
|
|
|
|
static void
|
|
track_element_added_cb (GESTrack * track, GESTrackElement * track_element,
|
|
GESTimeline * timeline)
|
|
{
|
|
/* Auto transition should be updated before we receive the signal */
|
|
g_signal_connect_after (GES_TRACK_ELEMENT (track_element), "notify::start",
|
|
G_CALLBACK (trackelement_start_changed_cb), timeline);
|
|
g_signal_connect_after (GES_TRACK_ELEMENT (track_element),
|
|
"notify::duration", G_CALLBACK (trackelement_duration_changed_cb),
|
|
timeline);
|
|
g_signal_connect_after (GES_TRACK_ELEMENT (track_element),
|
|
"notify::priority", G_CALLBACK (trackelement_priority_changed_cb),
|
|
timeline);
|
|
|
|
start_tracking_track_element (timeline, track_element);
|
|
}
|
|
|
|
static void
|
|
track_element_removed_cb (GESTrack * track,
|
|
GESTrackElement * track_element, GESTimeline * timeline)
|
|
{
|
|
|
|
if (GES_IS_SOURCE (track_element)) {
|
|
/* Make sure to reinitialise the moving context next time */
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
}
|
|
|
|
/* Disconnect all signal handlers */
|
|
g_signal_handlers_disconnect_by_func (track_element,
|
|
trackelement_start_changed_cb, timeline);
|
|
g_signal_handlers_disconnect_by_func (track_element,
|
|
trackelement_duration_changed_cb, timeline);
|
|
g_signal_handlers_disconnect_by_func (track_element,
|
|
trackelement_priority_changed_cb, timeline);
|
|
|
|
stop_tracking_track_element (timeline, track_element);
|
|
}
|
|
|
|
static GstPadProbeReturn
|
|
_pad_probe_cb (GstPad * mixer_pad, GstPadProbeInfo * info,
|
|
GESTimeline * timeline)
|
|
{
|
|
GstEvent *event = GST_PAD_PROBE_INFO_EVENT (info);
|
|
if (GST_EVENT_TYPE (event) == GST_EVENT_STREAM_START) {
|
|
LOCK_DYN (timeline);
|
|
if (timeline->priv->group_id == -1) {
|
|
if (!gst_event_parse_group_id (event, &timeline->priv->group_id))
|
|
timeline->priv->group_id = gst_util_group_id_next ();
|
|
}
|
|
|
|
info->data = gst_event_make_writable (event);
|
|
gst_event_set_group_id (GST_PAD_PROBE_INFO_EVENT (info),
|
|
timeline->priv->group_id);
|
|
UNLOCK_DYN (timeline);
|
|
|
|
return GST_PAD_PROBE_REMOVE;
|
|
}
|
|
|
|
return GST_PAD_PROBE_OK;
|
|
}
|
|
|
|
static void
|
|
_ghost_track_srcpad (TrackPrivate * tr_priv)
|
|
{
|
|
GstPad *pad;
|
|
gchar *padname;
|
|
gboolean no_more;
|
|
GList *tmp;
|
|
GESTrack *track = tr_priv->track;
|
|
|
|
pad = gst_element_get_static_pad (GST_ELEMENT (track), "src");
|
|
|
|
GST_DEBUG ("track:%p, pad:%s:%s", track, GST_DEBUG_PAD_NAME (pad));
|
|
|
|
/* Remember the pad */
|
|
LOCK_DYN (tr_priv->timeline);
|
|
GST_OBJECT_LOCK (track);
|
|
tr_priv->pad = pad;
|
|
|
|
no_more = TRUE;
|
|
for (tmp = tr_priv->timeline->priv->priv_tracks; tmp; tmp = g_list_next (tmp)) {
|
|
TrackPrivate *tr_priv = (TrackPrivate *) tmp->data;
|
|
|
|
if (!tr_priv->pad) {
|
|
GST_LOG ("Found track without pad %p", tr_priv->track);
|
|
no_more = FALSE;
|
|
}
|
|
}
|
|
GST_OBJECT_UNLOCK (track);
|
|
|
|
/* ghost it ! */
|
|
GST_DEBUG ("Ghosting pad and adding it to ourself");
|
|
padname = g_strdup_printf ("track_%p_src", track);
|
|
tr_priv->ghostpad = gst_ghost_pad_new (padname, pad);
|
|
g_free (padname);
|
|
gst_pad_set_active (tr_priv->ghostpad, TRUE);
|
|
gst_element_add_pad (GST_ELEMENT (tr_priv->timeline), tr_priv->ghostpad);
|
|
|
|
if (no_more) {
|
|
GST_DEBUG ("Signaling no-more-pads");
|
|
gst_element_no_more_pads (GST_ELEMENT (tr_priv->timeline));
|
|
}
|
|
|
|
tr_priv->probe_id = gst_pad_add_probe (pad,
|
|
GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM,
|
|
(GstPadProbeCallback) _pad_probe_cb, tr_priv->timeline, NULL);
|
|
|
|
UNLOCK_DYN (tr_priv->timeline);
|
|
}
|
|
|
|
gboolean
|
|
timeline_add_element (GESTimeline * timeline, GESTimelineElement * element)
|
|
{
|
|
GESTimelineElement *same_name =
|
|
g_hash_table_lookup (timeline->priv->all_elements,
|
|
element->name);
|
|
|
|
GST_DEBUG_OBJECT (timeline, "Adding element: %s", element->name);
|
|
if (same_name) {
|
|
GST_ERROR_OBJECT (timeline, "%s Already in the timeline %" GST_PTR_FORMAT,
|
|
element->name, same_name);
|
|
return FALSE;
|
|
}
|
|
|
|
g_hash_table_insert (timeline->priv->all_elements,
|
|
ges_timeline_element_get_name (element), gst_object_ref (element));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
timeline_remove_element (GESTimeline * timeline, GESTimelineElement * element)
|
|
{
|
|
return g_hash_table_remove (timeline->priv->all_elements, element->name);
|
|
}
|
|
|
|
void
|
|
timeline_fill_gaps (GESTimeline * timeline)
|
|
{
|
|
GList *tmp;
|
|
|
|
for (tmp = timeline->tracks; tmp; tmp = tmp->next) {
|
|
track_resort_and_fill_gaps (tmp->data);
|
|
}
|
|
}
|
|
|
|
/**** API *****/
|
|
/**
|
|
* ges_timeline_new:
|
|
*
|
|
* Creates a new empty #GESTimeline.
|
|
*
|
|
* Returns: The new timeline.
|
|
*/
|
|
|
|
GESTimeline *
|
|
ges_timeline_new (void)
|
|
{
|
|
GESProject *project = ges_project_new (NULL);
|
|
GESExtractable *timeline = g_object_new (GES_TYPE_TIMELINE, NULL);
|
|
|
|
ges_extractable_set_asset (timeline, GES_ASSET (project));
|
|
gst_object_unref (project);
|
|
|
|
return GES_TIMELINE (timeline);
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_new_from_uri:
|
|
* @uri: the URI to load from
|
|
* @error: (out) (allow-none): An error to be set in case something wrong happens or %NULL
|
|
*
|
|
* Creates a timeline from the given URI.
|
|
*
|
|
* Returns: A new timeline if the uri was loaded successfully, or NULL if the
|
|
* uri could not be loaded
|
|
*/
|
|
GESTimeline *
|
|
ges_timeline_new_from_uri (const gchar * uri, GError ** error)
|
|
{
|
|
GESTimeline *ret;
|
|
GESProject *project = ges_project_new (uri);
|
|
|
|
ret = GES_TIMELINE (ges_asset_extract (GES_ASSET (project), error));
|
|
gst_object_unref (project);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_load_from_uri:
|
|
* @timeline: an empty #GESTimeline into which to load the formatter
|
|
* @uri: The URI to load from
|
|
* @error: (out) (allow-none): An error to be set in case something wrong happens or %NULL
|
|
*
|
|
* Loads the contents of URI into the given timeline.
|
|
*
|
|
* Returns: TRUE if the timeline was loaded successfully, or FALSE if the uri
|
|
* could not be loaded.
|
|
*/
|
|
gboolean
|
|
ges_timeline_load_from_uri (GESTimeline * timeline, const gchar * uri,
|
|
GError ** error)
|
|
{
|
|
GESProject *project;
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), FALSE);
|
|
g_return_val_if_fail ((ges_extractable_get_asset (GES_EXTRACTABLE
|
|
(timeline)) == NULL), FALSE);
|
|
|
|
project = ges_project_new (uri);
|
|
ret = ges_project_load (project, timeline, error);
|
|
gst_object_unref (project);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_save_to_uri:
|
|
* @timeline: a #GESTimeline
|
|
* @uri: The location to save to
|
|
* @formatter_asset: (allow-none): The formatter asset to use or %NULL. If %NULL,
|
|
* will try to save in the same format as the one from which the timeline as been loaded
|
|
* or default to the formatter with highest rank
|
|
* @overwrite: %TRUE to overwrite file if it exists
|
|
* @error: (out) (allow-none): An error to be set in case something wrong happens or %NULL
|
|
*
|
|
* Saves the timeline to the given location
|
|
*
|
|
* Returns: TRUE if the timeline was successfully saved to the given location,
|
|
* else FALSE.
|
|
*/
|
|
gboolean
|
|
ges_timeline_save_to_uri (GESTimeline * timeline, const gchar * uri,
|
|
GESAsset * formatter_asset, gboolean overwrite, GError ** error)
|
|
{
|
|
GESProject *project;
|
|
|
|
gboolean ret, created_proj = FALSE;
|
|
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), FALSE);
|
|
project =
|
|
GES_PROJECT (ges_extractable_get_asset (GES_EXTRACTABLE (timeline)));
|
|
|
|
if (project == NULL) {
|
|
project = ges_project_new (NULL);
|
|
created_proj = TRUE;
|
|
}
|
|
|
|
ret = ges_project_save (project, timeline, uri, formatter_asset, overwrite,
|
|
error);
|
|
|
|
if (created_proj)
|
|
gst_object_unref (project);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_append_layer:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Append a newly created #GESLayer to @timeline
|
|
* Note that you do not own any reference to the returned layer.
|
|
*
|
|
* Returns: (transfer none): The newly created #GESLayer, or the last (empty)
|
|
* #GESLayer of @timeline.
|
|
*/
|
|
GESLayer *
|
|
ges_timeline_append_layer (GESTimeline * timeline)
|
|
{
|
|
guint32 priority;
|
|
GESLayer *layer;
|
|
|
|
layer = ges_layer_new ();
|
|
priority = g_list_length (timeline->layers);
|
|
ges_layer_set_priority (layer, priority);
|
|
|
|
ges_timeline_add_layer (timeline, layer);
|
|
|
|
return layer;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_add_layer:
|
|
* @timeline: a #GESTimeline
|
|
* @layer: the #GESLayer to add
|
|
*
|
|
* Add the layer to the timeline. The reference to the @layer will be stolen
|
|
* by the @timeline.
|
|
*
|
|
* Returns: TRUE if the layer was properly added, else FALSE.
|
|
*/
|
|
gboolean
|
|
ges_timeline_add_layer (GESTimeline * timeline, GESLayer * layer)
|
|
{
|
|
gboolean auto_transition;
|
|
GList *objects, *tmp;
|
|
|
|
GST_DEBUG ("timeline:%p, layer:%p", timeline, layer);
|
|
|
|
/* We can only add a layer that doesn't already belong to another timeline */
|
|
if (G_UNLIKELY (layer->timeline)) {
|
|
GST_WARNING ("Layer belongs to another timeline, can't add it");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Add to the list of layers, make sure we don't already control it */
|
|
if (G_UNLIKELY (g_list_find (timeline->layers, (gconstpointer) layer))) {
|
|
GST_WARNING ("Layer is already controlled by this timeline");
|
|
return FALSE;
|
|
}
|
|
|
|
auto_transition = ges_layer_get_auto_transition (layer);
|
|
|
|
/* If the user doesn't explicitely set layer auto_transition, then set our */
|
|
if (!auto_transition) {
|
|
auto_transition = ges_timeline_get_auto_transition (timeline);
|
|
ges_layer_set_auto_transition (layer, auto_transition);
|
|
}
|
|
|
|
gst_object_ref_sink (layer);
|
|
timeline->layers = g_list_insert_sorted (timeline->layers, layer,
|
|
(GCompareFunc) sort_layers);
|
|
|
|
/* Inform the layer that it belongs to a new timeline */
|
|
ges_layer_set_timeline (layer, timeline);
|
|
|
|
g_hash_table_insert (timeline->priv->by_layer, layer, g_sequence_new (NULL));
|
|
|
|
/* Connect to 'clip-added'/'clip-removed' signal from the new layer */
|
|
g_signal_connect_after (layer, "clip-added",
|
|
G_CALLBACK (layer_object_added_cb), timeline);
|
|
g_signal_connect_after (layer, "clip-removed",
|
|
G_CALLBACK (layer_object_removed_cb), timeline);
|
|
g_signal_connect (layer, "notify::priority",
|
|
G_CALLBACK (layer_priority_changed_cb), timeline);
|
|
g_signal_connect (layer, "notify::auto-transition",
|
|
G_CALLBACK (layer_auto_transition_changed_cb), timeline);
|
|
|
|
GST_DEBUG ("Done adding layer, emitting 'layer-added' signal");
|
|
g_signal_emit (timeline, ges_timeline_signals[LAYER_ADDED], 0, layer);
|
|
|
|
/* add any existing clips to the timeline */
|
|
objects = ges_layer_get_clips (layer);
|
|
for (tmp = objects; tmp; tmp = tmp->next) {
|
|
layer_object_added_cb (layer, tmp->data, timeline);
|
|
gst_object_unref (tmp->data);
|
|
tmp->data = NULL;
|
|
}
|
|
g_list_free (objects);
|
|
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_remove_layer:
|
|
* @timeline: a #GESTimeline
|
|
* @layer: the #GESLayer to remove
|
|
*
|
|
* Removes the layer from the timeline. The reference that the @timeline holds on
|
|
* the layer will be dropped. If you wish to use the @layer after calling this
|
|
* method, you need to take a reference before calling.
|
|
*
|
|
* Returns: TRUE if the layer was properly removed, else FALSE.
|
|
*/
|
|
|
|
gboolean
|
|
ges_timeline_remove_layer (GESTimeline * timeline, GESLayer * layer)
|
|
{
|
|
GList *layer_objects, *tmp;
|
|
|
|
GST_DEBUG ("timeline:%p, layer:%p", timeline, layer);
|
|
|
|
if (G_UNLIKELY (!g_list_find (timeline->layers, layer))) {
|
|
GST_WARNING ("Layer doesn't belong to this timeline");
|
|
return FALSE;
|
|
}
|
|
|
|
/* remove objects from any private data structures */
|
|
|
|
layer_objects = ges_layer_get_clips (layer);
|
|
for (tmp = layer_objects; tmp; tmp = tmp->next) {
|
|
layer_object_removed_cb (layer, GES_CLIP (tmp->data), timeline);
|
|
gst_object_unref (G_OBJECT (tmp->data));
|
|
tmp->data = NULL;
|
|
}
|
|
g_list_free (layer_objects);
|
|
|
|
/* Disconnect signals */
|
|
GST_DEBUG ("Disconnecting signal callbacks");
|
|
g_signal_handlers_disconnect_by_func (layer, layer_object_added_cb, timeline);
|
|
g_signal_handlers_disconnect_by_func (layer, layer_object_removed_cb,
|
|
timeline);
|
|
g_signal_handlers_disconnect_by_func (layer, layer_priority_changed_cb,
|
|
timeline);
|
|
g_signal_handlers_disconnect_by_func (layer,
|
|
layer_auto_transition_changed_cb, timeline);
|
|
|
|
g_hash_table_remove (timeline->priv->by_layer, layer);
|
|
timeline->layers = g_list_remove (timeline->layers, layer);
|
|
ges_layer_set_timeline (layer, NULL);
|
|
|
|
g_signal_emit (timeline, ges_timeline_signals[LAYER_REMOVED], 0, layer);
|
|
|
|
gst_object_unref (layer);
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_add_track:
|
|
* @timeline: a #GESTimeline
|
|
* @track: the #GESTrack to add
|
|
*
|
|
* Add a track to the timeline. The reference to the track will be stolen by the
|
|
* pipeline.
|
|
*
|
|
* Returns: TRUE if the track was properly added, else FALSE.
|
|
*/
|
|
|
|
/* FIXME: create track elements for clips which have already been
|
|
* added to existing layers.
|
|
*/
|
|
|
|
gboolean
|
|
ges_timeline_add_track (GESTimeline * timeline, GESTrack * track)
|
|
{
|
|
TrackPrivate *tr_priv;
|
|
GList *tmp;
|
|
|
|
GST_DEBUG ("timeline:%p, track:%p", timeline, track);
|
|
|
|
/* make sure we don't already control it */
|
|
if (G_UNLIKELY (g_list_find (timeline->tracks, (gconstpointer) track))) {
|
|
GST_WARNING ("Track is already controlled by this timeline");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Add the track to ourself (as a GstBin)
|
|
* Reference is stolen ! */
|
|
if (G_UNLIKELY (!gst_bin_add (GST_BIN (timeline), GST_ELEMENT (track)))) {
|
|
GST_WARNING ("Couldn't add track to ourself (GST)");
|
|
return FALSE;
|
|
}
|
|
|
|
tr_priv = g_new0 (TrackPrivate, 1);
|
|
tr_priv->timeline = timeline;
|
|
tr_priv->track = track;
|
|
|
|
/* Add the track to the list of tracks we track */
|
|
LOCK_DYN (timeline);
|
|
timeline->priv->priv_tracks = g_list_append (timeline->priv->priv_tracks,
|
|
tr_priv);
|
|
UNLOCK_DYN (timeline);
|
|
timeline->tracks = g_list_append (timeline->tracks, track);
|
|
|
|
/* Inform the track that it's currently being used by ourself */
|
|
ges_track_set_timeline (track, timeline);
|
|
|
|
GST_DEBUG ("Done adding track, emitting 'track-added' signal");
|
|
|
|
_ghost_track_srcpad (tr_priv);
|
|
|
|
/* emit 'track-added' */
|
|
g_signal_emit (timeline, ges_timeline_signals[TRACK_ADDED], 0, track);
|
|
|
|
/* ensure that each existing clip has the opportunity to create a
|
|
* track element for this track*/
|
|
|
|
/* We connect to the object for the timeline editing mode management */
|
|
g_signal_connect (G_OBJECT (track), "track-element-added",
|
|
G_CALLBACK (track_element_added_cb), timeline);
|
|
g_signal_connect (G_OBJECT (track), "track-element-removed",
|
|
G_CALLBACK (track_element_removed_cb), timeline);
|
|
|
|
for (tmp = timeline->layers; tmp; tmp = tmp->next) {
|
|
GList *objects, *obj;
|
|
objects = ges_layer_get_clips (tmp->data);
|
|
|
|
for (obj = objects; obj; obj = obj->next) {
|
|
GESClip *clip = obj->data;
|
|
|
|
add_object_to_tracks (timeline, clip, track);
|
|
gst_object_unref (clip);
|
|
}
|
|
g_list_free (objects);
|
|
}
|
|
|
|
/* FIXME Check if we should rollback if we can't sync state */
|
|
gst_element_sync_state_with_parent (GST_ELEMENT (track));
|
|
g_object_set (track, "message-forward", TRUE, NULL);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_remove_track:
|
|
* @timeline: a #GESTimeline
|
|
* @track: the #GESTrack to remove
|
|
*
|
|
* Remove the @track from the @timeline. The reference stolen when adding the
|
|
* @track will be removed. If you wish to use the @track after calling this
|
|
* function you must ensure that you have a reference to it.
|
|
*
|
|
* Returns: TRUE if the @track was properly removed, else FALSE.
|
|
*/
|
|
|
|
/* FIXME: release any track elements associated with this layer. currenly this
|
|
* will not happen if you remove the track before removing *all*
|
|
* clips which have a track element in this track.
|
|
*/
|
|
|
|
gboolean
|
|
ges_timeline_remove_track (GESTimeline * timeline, GESTrack * track)
|
|
{
|
|
GList *tmp;
|
|
TrackPrivate *tr_priv;
|
|
GESTimelinePrivate *priv;
|
|
|
|
g_return_val_if_fail (GES_IS_TRACK (track), FALSE);
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), FALSE);
|
|
|
|
GST_DEBUG ("timeline:%p, track:%p", timeline, track);
|
|
|
|
priv = timeline->priv;
|
|
LOCK_DYN (timeline);
|
|
if (G_UNLIKELY (!(tmp = g_list_find_custom (priv->priv_tracks,
|
|
track, (GCompareFunc) custom_find_track)))) {
|
|
GST_WARNING ("Track doesn't belong to this timeline");
|
|
UNLOCK_DYN (timeline);
|
|
return FALSE;
|
|
}
|
|
|
|
tr_priv = tmp->data;
|
|
gst_object_unref (tr_priv->pad);
|
|
priv->priv_tracks = g_list_remove (priv->priv_tracks, tr_priv);
|
|
UNLOCK_DYN (timeline);
|
|
timeline->tracks = g_list_remove (timeline->tracks, track);
|
|
|
|
ges_track_set_timeline (track, NULL);
|
|
|
|
/* Remove ghost pad */
|
|
if (tr_priv->ghostpad) {
|
|
GST_DEBUG ("Removing ghostpad");
|
|
gst_pad_set_active (tr_priv->ghostpad, FALSE);
|
|
gst_ghost_pad_set_target ((GstGhostPad *) tr_priv->ghostpad, NULL);
|
|
gst_element_remove_pad (GST_ELEMENT (timeline), tr_priv->ghostpad);
|
|
}
|
|
|
|
/* Remove pad-added/-removed handlers */
|
|
g_signal_handlers_disconnect_by_func (track, track_element_added_cb,
|
|
timeline);
|
|
g_signal_handlers_disconnect_by_func (track, track_element_removed_cb,
|
|
timeline);
|
|
|
|
/* Signal track removal to all layers/objects */
|
|
g_signal_emit (timeline, ges_timeline_signals[TRACK_REMOVED], 0, track);
|
|
|
|
/* remove track from our bin */
|
|
gst_object_ref (track);
|
|
if (G_UNLIKELY (!gst_bin_remove (GST_BIN (timeline), GST_ELEMENT (track)))) {
|
|
GST_WARNING ("Couldn't remove track to ourself (GST)");
|
|
gst_object_unref (track);
|
|
return FALSE;
|
|
}
|
|
|
|
/* set track state to NULL */
|
|
gst_element_set_state (GST_ELEMENT (track), GST_STATE_NULL);
|
|
|
|
gst_object_unref (track);
|
|
|
|
g_free (tr_priv);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_track_for_pad:
|
|
* @timeline: The #GESTimeline
|
|
* @pad: The #GstPad
|
|
*
|
|
* Search the #GESTrack corresponding to the given @timeline's @pad.
|
|
*
|
|
* Returns: (transfer none): The corresponding #GESTrack if it is found,
|
|
* or %NULL if there is an error.
|
|
*/
|
|
|
|
GESTrack *
|
|
ges_timeline_get_track_for_pad (GESTimeline * timeline, GstPad * pad)
|
|
{
|
|
GList *tmp;
|
|
|
|
LOCK_DYN (timeline);
|
|
for (tmp = timeline->priv->priv_tracks; tmp; tmp = g_list_next (tmp)) {
|
|
TrackPrivate *tr_priv = (TrackPrivate *) tmp->data;
|
|
if (pad == tr_priv->ghostpad) {
|
|
UNLOCK_DYN (timeline);
|
|
return tr_priv->track;
|
|
}
|
|
}
|
|
UNLOCK_DYN (timeline);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_pad_for_track:
|
|
* @timeline: The #GESTimeline
|
|
* @track: The #GESTrack
|
|
*
|
|
* Search the #GstPad corresponding to the given @timeline's @track.
|
|
*
|
|
* Returns: (transfer none): The corresponding #GstPad if it is found,
|
|
* or %NULL if there is an error.
|
|
*/
|
|
|
|
GstPad *
|
|
ges_timeline_get_pad_for_track (GESTimeline * timeline, GESTrack * track)
|
|
{
|
|
GList *tmp;
|
|
|
|
LOCK_DYN (timeline);
|
|
for (tmp = timeline->priv->priv_tracks; tmp; tmp = g_list_next (tmp)) {
|
|
TrackPrivate *tr_priv = (TrackPrivate *) tmp->data;
|
|
|
|
if (track == tr_priv->track) {
|
|
if (tr_priv->ghostpad)
|
|
gst_object_ref (tr_priv->ghostpad);
|
|
|
|
UNLOCK_DYN (timeline);
|
|
return tr_priv->ghostpad;
|
|
}
|
|
}
|
|
UNLOCK_DYN (timeline);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_tracks:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Returns the list of #GESTrack used by the Timeline.
|
|
*
|
|
* Returns: (transfer full) (element-type GESTrack): A list of #GESTrack.
|
|
* The caller should unref each track once he is done with them.
|
|
*/
|
|
GList *
|
|
ges_timeline_get_tracks (GESTimeline * timeline)
|
|
{
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), NULL);
|
|
|
|
return g_list_copy_deep (timeline->tracks, (GCopyFunc) gst_object_ref, NULL);
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_layers:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Get the list of #GESLayer present in the Timeline.
|
|
*
|
|
* Returns: (transfer full) (element-type GESLayer): the list of
|
|
* #GESLayer present in the Timeline sorted by priority.
|
|
* The caller should unref each Layer once he is done with them.
|
|
*/
|
|
GList *
|
|
ges_timeline_get_layers (GESTimeline * timeline)
|
|
{
|
|
GList *tmp, *res = NULL;
|
|
|
|
for (tmp = timeline->layers; tmp; tmp = g_list_next (tmp)) {
|
|
res = g_list_insert_sorted (res, gst_object_ref (tmp->data),
|
|
(GCompareFunc) sort_layers);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static void
|
|
track_commited_cb (GESTrack * track, GESTimeline * timeline)
|
|
{
|
|
gboolean emit_commited = FALSE;
|
|
GST_OBJECT_LOCK (timeline);
|
|
timeline->priv->expected_commited -= 1;
|
|
if (timeline->priv->expected_commited == 0)
|
|
emit_commited = TRUE;
|
|
g_signal_handlers_disconnect_by_func (track, track_commited_cb, timeline);
|
|
GST_OBJECT_UNLOCK (timeline);
|
|
|
|
if (emit_commited) {
|
|
g_signal_emit (timeline, ges_timeline_signals[COMMITED], 0);
|
|
}
|
|
}
|
|
|
|
/* Must be called with the timeline's DYN_LOCK */
|
|
static gboolean
|
|
ges_timeline_commit_unlocked (GESTimeline * timeline)
|
|
{
|
|
GList *tmp;
|
|
gboolean res = TRUE;
|
|
|
|
GST_DEBUG_OBJECT (timeline, "commiting changes");
|
|
|
|
for (tmp = timeline->layers; tmp; tmp = tmp->next) {
|
|
_create_transitions_on_layer (timeline, GES_LAYER (tmp->data),
|
|
NULL, NULL, _find_transition_from_auto_transitions);
|
|
}
|
|
|
|
timeline->priv->expected_commited =
|
|
g_list_length (timeline->priv->priv_tracks);
|
|
|
|
if (timeline->priv->expected_commited == 0) {
|
|
g_signal_emit (timeline, ges_timeline_signals[COMMITED], 0);
|
|
} else {
|
|
for (tmp = timeline->tracks; tmp; tmp = tmp->next) {
|
|
g_signal_connect (tmp->data, "commited", G_CALLBACK (track_commited_cb),
|
|
timeline);
|
|
if (!ges_track_commit (GES_TRACK (tmp->data)))
|
|
res = FALSE;
|
|
}
|
|
}
|
|
|
|
/* Make sure we reset the context */
|
|
timeline->priv->movecontext.needs_move_ctx = TRUE;
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_commit:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Commit all the pending changes of the clips contained in the
|
|
* @timeline.
|
|
*
|
|
* When changes happen in a timeline, they are not
|
|
* directly executed in the non-linear engine. Call this method once you are
|
|
* done with a set of changes and want it to be executed.
|
|
*
|
|
* The GESTimeline::commited signal will be emitted when the (possibly updated)
|
|
* #GstPipeline is ready to output data again, except if the state of the
|
|
* timeline was #GST_STATE_READY or #GST_STATE_NULL.
|
|
*
|
|
* Note that all the pending changes will automatically be executed when the
|
|
* timeline goes from #GST_STATE_READY to #GST_STATE_PAUSED, which usually is
|
|
* triggered by corresponding state changes in a containing #GESPipeline.
|
|
*
|
|
* You should not try to change the state of the timeline, seek it or add
|
|
* tracks to it during a commit operation, that is between a call to this
|
|
* function and after receiving the GESTimeline::commited signal.
|
|
*
|
|
* See #ges_timeline_commit_sync if you don't want to bother with waiting
|
|
* for the signal.
|
|
*
|
|
* Returns: %TRUE if pending changes were commited or %FALSE if nothing needed
|
|
* to be commited
|
|
*/
|
|
gboolean
|
|
ges_timeline_commit (GESTimeline * timeline)
|
|
{
|
|
gboolean ret;
|
|
|
|
LOCK_DYN (timeline);
|
|
ret = ges_timeline_commit_unlocked (timeline);
|
|
UNLOCK_DYN (timeline);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
commited_cb (GESTimeline * timeline)
|
|
{
|
|
g_mutex_lock (&timeline->priv->commited_lock);
|
|
g_cond_signal (&timeline->priv->commited_cond);
|
|
g_mutex_unlock (&timeline->priv->commited_lock);
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_commit_sync:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Commit all the pending changes of the #GESClips contained in the
|
|
* @timeline.
|
|
*
|
|
* Will return once the update is complete, that is when the
|
|
* (possibly updated) #GstPipeline is ready to output data again, or if the
|
|
* state of the timeline was #GST_STATE_READY or #GST_STATE_NULL.
|
|
*
|
|
* This function will wait for any pending state change of the timeline by
|
|
* calling #gst_element_get_state with a #GST_CLOCK_TIME_NONE timeout, you
|
|
* should not try to change the state from another thread before this function
|
|
* has returned.
|
|
*
|
|
* See #ges_timeline_commit for more information.
|
|
*
|
|
* Returns: %TRUE if pending changes were commited or %FALSE if nothing needed
|
|
* to be commited
|
|
*/
|
|
gboolean
|
|
ges_timeline_commit_sync (GESTimeline * timeline)
|
|
{
|
|
gboolean ret;
|
|
gboolean wait_for_signal;
|
|
|
|
/* Let's make sure our state is stable */
|
|
gst_element_get_state (GST_ELEMENT (timeline), NULL, NULL,
|
|
GST_CLOCK_TIME_NONE);
|
|
|
|
/* Let's make sure no track gets added between now and the actual commiting */
|
|
LOCK_DYN (timeline);
|
|
wait_for_signal = g_list_length (timeline->priv->priv_tracks) > 0
|
|
&& GST_STATE (timeline) >= GST_STATE_PAUSED;
|
|
|
|
if (!wait_for_signal) {
|
|
ret = ges_timeline_commit_unlocked (timeline);
|
|
} else {
|
|
gulong handler_id =
|
|
g_signal_connect (timeline, "commited", (GCallback) commited_cb, NULL);
|
|
|
|
g_mutex_lock (&timeline->priv->commited_lock);
|
|
|
|
ret = ges_timeline_commit_unlocked (timeline);
|
|
g_cond_wait (&timeline->priv->commited_cond,
|
|
&timeline->priv->commited_lock);
|
|
g_mutex_unlock (&timeline->priv->commited_lock);
|
|
g_signal_handler_disconnect (timeline, handler_id);
|
|
}
|
|
|
|
UNLOCK_DYN (timeline);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_duration:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Get the current duration of @timeline
|
|
*
|
|
* Returns: The current duration of @timeline
|
|
*/
|
|
GstClockTime
|
|
ges_timeline_get_duration (GESTimeline * timeline)
|
|
{
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), GST_CLOCK_TIME_NONE);
|
|
|
|
return timeline->priv->duration;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_auto_transition:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Gets whether transitions are automatically added when objects
|
|
* overlap or not.
|
|
*
|
|
* Returns: %TRUE if transitions are automatically added, else %FALSE.
|
|
*/
|
|
gboolean
|
|
ges_timeline_get_auto_transition (GESTimeline * timeline)
|
|
{
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), 0);
|
|
|
|
return timeline->priv->auto_transition;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_set_auto_transition:
|
|
* @timeline: a #GESLayer
|
|
* @auto_transition: whether the auto_transition is active
|
|
*
|
|
* Sets the layer to the given @auto_transition. See the documentation of the
|
|
* property auto_transition for more information.
|
|
*/
|
|
void
|
|
ges_timeline_set_auto_transition (GESTimeline * timeline,
|
|
gboolean auto_transition)
|
|
{
|
|
GList *layers;
|
|
GESLayer *layer;
|
|
|
|
g_return_if_fail (GES_IS_TIMELINE (timeline));
|
|
|
|
timeline->priv->auto_transition = auto_transition;
|
|
g_object_notify (G_OBJECT (timeline), "auto-transition");
|
|
|
|
layers = timeline->layers;
|
|
for (; layers; layers = layers->next) {
|
|
layer = layers->data;
|
|
ges_layer_set_auto_transition (layer, auto_transition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_snapping_distance:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Gets the configured snapping distance of the timeline. See
|
|
* the documentation of the property snapping_distance for more
|
|
* information.
|
|
*
|
|
* Returns: The @snapping_distance property of the timeline
|
|
*/
|
|
GstClockTime
|
|
ges_timeline_get_snapping_distance (GESTimeline * timeline)
|
|
{
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), GST_CLOCK_TIME_NONE);
|
|
|
|
return timeline->priv->snapping_distance;
|
|
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_set_snapping_distance:
|
|
* @timeline: a #GESLayer
|
|
* @snapping_distance: whether the snapping_distance is active
|
|
*
|
|
* Sets the @snapping_distance of the timeline. See the documentation of the
|
|
* property snapping_distance for more information.
|
|
*/
|
|
void
|
|
ges_timeline_set_snapping_distance (GESTimeline * timeline,
|
|
GstClockTime snapping_distance)
|
|
{
|
|
g_return_if_fail (GES_IS_TIMELINE (timeline));
|
|
|
|
timeline->priv->snapping_distance = snapping_distance;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_element:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Gets a #GESTimelineElement contained in the timeline
|
|
*
|
|
* Returns: (transfer full): The #GESTimelineElement or %NULL if
|
|
* not found.
|
|
*/
|
|
GESTimelineElement *
|
|
ges_timeline_get_element (GESTimeline * timeline, const gchar * name)
|
|
{
|
|
GESTimelineElement *ret;
|
|
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), NULL);
|
|
|
|
ret = g_hash_table_lookup (timeline->priv->all_elements, name);
|
|
|
|
if (ret)
|
|
return gst_object_ref (ret);
|
|
|
|
#ifndef GST_DISABLE_GST_DEBUG
|
|
{
|
|
GList *element_names, *tmp;
|
|
element_names = g_hash_table_get_keys (timeline->priv->all_elements);
|
|
|
|
GST_INFO_OBJECT (timeline, "Does not contain element %s", name);
|
|
|
|
for (tmp = element_names; tmp; tmp = tmp->next) {
|
|
GST_DEBUG_OBJECT (timeline, "Containes: %s", (gchar *) tmp->data);
|
|
}
|
|
g_list_free (element_names);
|
|
}
|
|
#endif
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_is_empty:
|
|
* @timeline: a #GESTimeline
|
|
*
|
|
* Check whether a #GESTimelineElement is empty or not
|
|
*
|
|
* Returns: %TRUE if the timeline is empty %FALSE otherwize
|
|
*/
|
|
gboolean
|
|
ges_timeline_is_empty (GESTimeline * timeline)
|
|
{
|
|
GHashTableIter iter;
|
|
gpointer key, value;
|
|
|
|
g_return_val_if_fail (GES_IS_TIMELINE (timeline), FALSE);
|
|
|
|
if (g_hash_table_size (timeline->priv->all_elements) == 0)
|
|
return TRUE;
|
|
|
|
g_hash_table_iter_init (&iter, timeline->priv->all_elements);
|
|
while (g_hash_table_iter_next (&iter, &key, &value)) {
|
|
if (GES_IS_SOURCE (value) &&
|
|
ges_track_element_is_active (GES_TRACK_ELEMENT (value)))
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* ges_timeline_get_layer:
|
|
* @timeline: The #GESTimeline to retrive a layer from
|
|
* @priority: The priority of the layer to find
|
|
*
|
|
* Retrieve the layer with @priority as a priority
|
|
*
|
|
* Returns: A #GESLayer or %NULL if no layer with @priority was found
|
|
*
|
|
* Since 1.6
|
|
*/
|
|
GESLayer *
|
|
ges_timeline_get_layer (GESTimeline * timeline, guint priority)
|
|
{
|
|
GList *tmp;
|
|
GESLayer *layer = NULL;
|
|
|
|
for (tmp = timeline->layers; tmp; tmp = tmp->next) {
|
|
GESLayer *tmp_layer = GES_LAYER (tmp->data);
|
|
guint tmp_priority;
|
|
|
|
g_object_get (tmp_layer, "priority", &tmp_priority, NULL);
|
|
if (tmp_priority == priority) {
|
|
layer = gst_object_ref (tmp_layer);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return layer;
|
|
}
|