mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-27 04:01:08 +00:00
52f7150de4
The previous code was storing container children in reverse addition order, this was mitigated by the fact that track elements were also stored in reverse order, thus restoring the original order, but it seems more consistent to preserve order throughout, the extra cost of append operations is negligible. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/1549>
1089 lines
35 KiB
C
1089 lines
35 KiB
C
/* GStreamer Editing Services
|
|
* Copyright (C) <2013> Thibault Saunier <thibault.saunier@collabora.com>
|
|
* <2013> Collabora Ltd.
|
|
*
|
|
* 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:gescontainer
|
|
* @title: GESContainer
|
|
* @short_description: Base Class for elements responsible for controlling
|
|
* other #GESTimelineElement-s
|
|
*
|
|
* A #GESContainer is a timeline element that controls other
|
|
* #GESTimelineElement-s, which are its children. In particular, it is
|
|
* responsible for maintaining the relative #GESTimelineElement:start and
|
|
* #GESTimelineElement:duration times of its children. Therefore, if a
|
|
* container is temporally adjusted or moved to a new layer, it may
|
|
* accordingly adjust and move its children. Similarly, a change in one of
|
|
* its children may prompt the parent to correspondingly change its
|
|
* siblings.
|
|
*/
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "ges-container.h"
|
|
#include "ges.h"
|
|
#include "ges-internal.h"
|
|
|
|
#include <string.h>
|
|
|
|
GST_DEBUG_CATEGORY_STATIC (ges_container_debug);
|
|
#undef GST_CAT_DEFAULT
|
|
#define GST_CAT_DEFAULT ges_container_debug
|
|
|
|
/* Mapping of relationship between a Container and the TimelineElements
|
|
* it controls
|
|
*
|
|
* NOTE : Does it make sense to make it public in the future ?
|
|
*/
|
|
typedef struct
|
|
{
|
|
GESTimelineElement *child;
|
|
|
|
GstClockTime start_offset;
|
|
GstClockTime duration_offset;
|
|
|
|
gulong start_notifyid;
|
|
gulong duration_notifyid;
|
|
gulong child_property_added_notifyid;
|
|
gulong child_property_removed_notifyid;
|
|
} ChildMapping;
|
|
|
|
enum
|
|
{
|
|
CHILD_ADDED_SIGNAL,
|
|
CHILD_REMOVED_SIGNAL,
|
|
LAST_SIGNAL
|
|
};
|
|
|
|
static guint ges_container_signals[LAST_SIGNAL] = { 0 };
|
|
|
|
struct _GESContainerPrivate
|
|
{
|
|
/*< public > */
|
|
GESLayer *layer;
|
|
|
|
/*< private > */
|
|
/* Set to TRUE when the container is doing updates of track object
|
|
* properties so we don't end up in infinite property update loops
|
|
*/
|
|
GHashTable *mappings;
|
|
|
|
/* List of GESTimelineElement being in the "child-added" signal
|
|
* emission stage */
|
|
GList *adding_children;
|
|
|
|
GList *copied_children;
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_HEIGHT,
|
|
PROP_LAST
|
|
};
|
|
|
|
static GParamSpec *properties[PROP_LAST];
|
|
|
|
G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GESContainer, ges_container,
|
|
GES_TYPE_TIMELINE_ELEMENT);
|
|
|
|
/************************
|
|
* Private methods *
|
|
************************/
|
|
static void
|
|
_free_mapping (ChildMapping * mapping)
|
|
{
|
|
GESTimelineElement *child = mapping->child;
|
|
|
|
/* Disconnect all notify listeners */
|
|
if (mapping->start_notifyid)
|
|
g_signal_handler_disconnect (child, mapping->start_notifyid);
|
|
if (mapping->duration_notifyid)
|
|
g_signal_handler_disconnect (child, mapping->duration_notifyid);
|
|
if (mapping->child_property_added_notifyid)
|
|
g_signal_handler_disconnect (child, mapping->child_property_added_notifyid);
|
|
if (mapping->child_property_removed_notifyid)
|
|
g_signal_handler_disconnect (child,
|
|
mapping->child_property_removed_notifyid);
|
|
if (child) {
|
|
ges_timeline_element_set_parent (child, NULL);
|
|
gst_object_unref (child);
|
|
}
|
|
|
|
g_slice_free (ChildMapping, mapping);
|
|
}
|
|
|
|
static gint
|
|
compare_grouping_prio (GType * a, GType * b)
|
|
{
|
|
gint ret = 0;
|
|
GObjectClass *aclass = g_type_class_ref (*a);
|
|
GObjectClass *bclass = g_type_class_ref (*b);
|
|
|
|
/* We want higher prios to be first */
|
|
if (GES_CONTAINER_CLASS (aclass)->grouping_priority <
|
|
GES_CONTAINER_CLASS (bclass)->grouping_priority)
|
|
ret = 1;
|
|
else if (GES_CONTAINER_CLASS (aclass)->grouping_priority >
|
|
GES_CONTAINER_CLASS (bclass)->grouping_priority)
|
|
ret = -1;
|
|
|
|
g_type_class_unref (aclass);
|
|
g_type_class_unref (bclass);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
_resync_position_offsets (GESTimelineElement * child,
|
|
ChildMapping * map, GESContainer * container)
|
|
{
|
|
map->start_offset = _START (container) - _START (child);
|
|
map->duration_offset = _DURATION (container) - _DURATION (child);
|
|
}
|
|
|
|
/*****************************************************
|
|
* *
|
|
* GESTimelineElement virtual methods implementation *
|
|
* *
|
|
*****************************************************/
|
|
static gboolean
|
|
_set_start (GESTimelineElement * element, GstClockTime start)
|
|
{
|
|
GList *tmp;
|
|
ChildMapping *map;
|
|
GESContainer *container = GES_CONTAINER (element);
|
|
GESContainerPrivate *priv = container->priv;
|
|
|
|
GST_DEBUG_OBJECT (element, "Updating children offsets, (initiated_move: %"
|
|
GST_PTR_FORMAT ")", container->initiated_move);
|
|
|
|
for (tmp = container->children; tmp; tmp = g_list_next (tmp)) {
|
|
GESTimelineElement *child = (GESTimelineElement *) tmp->data;
|
|
|
|
map = g_hash_table_lookup (priv->mappings, child);
|
|
map->start_offset = start - _START (child);
|
|
}
|
|
container->children_control_mode = GES_CHILDREN_UPDATE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
_set_duration (GESTimelineElement * element, GstClockTime duration)
|
|
{
|
|
GList *tmp;
|
|
GESContainer *container = GES_CONTAINER (element);
|
|
GESContainerPrivate *priv = container->priv;
|
|
|
|
for (tmp = container->children; tmp; tmp = g_list_next (tmp)) {
|
|
GESTimelineElement *child = (GESTimelineElement *) tmp->data;
|
|
ChildMapping *map = g_hash_table_lookup (priv->mappings, child);
|
|
|
|
map->duration_offset = duration - _DURATION (child);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
_add_childs_child_property (GESTimelineElement * container_child,
|
|
GObject * prop_child, GParamSpec * property, GESContainer * container)
|
|
{
|
|
/* the container_child is kept as the owner of this child property when
|
|
* we register it on ourselves, but we use the same GObject child
|
|
* instance who the property comes from */
|
|
gboolean res =
|
|
ges_timeline_element_add_child_property_full (GES_TIMELINE_ELEMENT
|
|
(container), container_child, property, prop_child);
|
|
if (!res)
|
|
GST_INFO_OBJECT (container, "Could not register the child property '%s' "
|
|
"of our child %" GES_FORMAT " for the object %" GST_PTR_FORMAT,
|
|
property->name, GES_ARGS (container_child), prop_child);
|
|
}
|
|
|
|
static void
|
|
_ges_container_add_child_properties (GESContainer * container,
|
|
GESTimelineElement * child)
|
|
{
|
|
guint n_props, i;
|
|
|
|
/* use get_children_properties, rather than list_children_properties
|
|
* to ensure we are getting all the properties, without any interference
|
|
* from the ->list_children_properties vmethods */
|
|
GParamSpec **child_props =
|
|
ges_timeline_element_get_children_properties (child,
|
|
&n_props);
|
|
|
|
for (i = 0; i < n_props; i++) {
|
|
GParamSpec *property = child_props[i];
|
|
GObject *prop_child =
|
|
ges_timeline_element_get_child_from_child_property (child, property);
|
|
if (prop_child)
|
|
_add_childs_child_property (child, prop_child, property, container);
|
|
g_param_spec_unref (property);
|
|
}
|
|
|
|
g_free (child_props);
|
|
}
|
|
|
|
static void
|
|
_remove_childs_child_property (GESTimelineElement * container_child,
|
|
GObject * prop_child, GParamSpec * property, GESContainer * container)
|
|
{
|
|
/* NOTE: some children may share the same GParamSpec. Currently, only
|
|
* the first such child added will have its children properties
|
|
* successfully registered for the container (even though the GObject
|
|
* child who the properties belong to will be a different instance). As
|
|
* such, we only want to remove the child property if it corresponds to
|
|
* the same instance that the parent container has.
|
|
* E.g. if we add child1 and child2, that have the same (or some
|
|
* overlapping) children properties. And child1 is added before child2,
|
|
* then child2's overlapping children properties would not be registered.
|
|
* If we remove child2, we do *not* want to removed the child properties
|
|
* for child1 because they belong to a GObject instance that we still
|
|
* have in our control.
|
|
* If we remove child1, we *do* want to remove the child properties for
|
|
* child1, even though child2 may overlap with some of them, because we
|
|
* are loosing the specific GObject instance that it belongs to!
|
|
* We could try and register the ones that match for the other children.
|
|
* However, it is probably simpler to change
|
|
* ges_timeline_element_add_child_property_full to accept the same
|
|
* GParamSpec, for different instances.
|
|
*/
|
|
GESTimelineElement *element = GES_TIMELINE_ELEMENT (container);
|
|
GObject *our_prop_child =
|
|
ges_timeline_element_get_child_from_child_property (element, property);
|
|
if (our_prop_child == prop_child)
|
|
ges_timeline_element_remove_child_property (element, property);
|
|
else
|
|
GST_INFO_OBJECT (container, "Not removing child property '%s' for child"
|
|
" %" GES_FORMAT " because it derives from the object %" GST_PTR_FORMAT
|
|
"(%p) rather than the object %" GST_PTR_FORMAT "(%p)", property->name,
|
|
GES_ARGS (container_child), prop_child, prop_child, our_prop_child,
|
|
our_prop_child);
|
|
}
|
|
|
|
static void
|
|
_ges_container_remove_child_properties (GESContainer * container,
|
|
GESTimelineElement * child)
|
|
{
|
|
guint n_props, i;
|
|
|
|
/* use get_children_properties, rather than list_children_properties
|
|
* to ensure we are getting all the properties, without any interference
|
|
* from the ->list_children_properties vmethods */
|
|
GParamSpec **child_props =
|
|
ges_timeline_element_get_children_properties (child,
|
|
&n_props);
|
|
|
|
for (i = 0; i < n_props; i++) {
|
|
GParamSpec *property = child_props[i];
|
|
GObject *prop_child =
|
|
ges_timeline_element_get_child_from_child_property (child, property);
|
|
if (prop_child)
|
|
_remove_childs_child_property (child, prop_child, property, container);
|
|
g_param_spec_unref (property);
|
|
}
|
|
|
|
g_free (child_props);
|
|
}
|
|
|
|
static gboolean
|
|
_lookup_child (GESTimelineElement * self, const gchar * prop_name,
|
|
GObject ** child, GParamSpec ** pspec)
|
|
{
|
|
GList *tmp;
|
|
|
|
/* FIXME Implement a syntax to precisely get properties by path */
|
|
for (tmp = GES_CONTAINER_CHILDREN (self); tmp; tmp = tmp->next) {
|
|
if (ges_timeline_element_lookup_child (tmp->data, prop_name, child, pspec))
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static GESTrackType
|
|
_get_track_types (GESTimelineElement * object)
|
|
{
|
|
GESTrackType types = GES_TRACK_TYPE_UNKNOWN;
|
|
GList *tmp, *children = ges_container_get_children (GES_CONTAINER (object),
|
|
TRUE);
|
|
|
|
for (tmp = children; tmp; tmp = tmp->next) {
|
|
if (GES_IS_TRACK_ELEMENT (tmp->data)) {
|
|
types |= ges_timeline_element_get_track_types (tmp->data);
|
|
}
|
|
}
|
|
|
|
g_list_free_full (children, gst_object_unref);
|
|
|
|
return types ^ GES_TRACK_TYPE_UNKNOWN;
|
|
}
|
|
|
|
static void
|
|
_deep_copy (GESTimelineElement * element, GESTimelineElement * copy)
|
|
{
|
|
GList *tmp;
|
|
GESContainer *self = GES_CONTAINER (element), *ccopy = GES_CONTAINER (copy);
|
|
|
|
for (tmp = GES_CONTAINER_CHILDREN (element); tmp; tmp = tmp->next) {
|
|
ChildMapping *map, *orig_map;
|
|
orig_map = g_hash_table_lookup (self->priv->mappings, tmp->data);
|
|
map = g_slice_new0 (ChildMapping);
|
|
map->child = ges_timeline_element_copy (tmp->data, TRUE);
|
|
map->start_offset = orig_map->start_offset;
|
|
|
|
ccopy->priv->copied_children = g_list_prepend (ccopy->priv->copied_children,
|
|
map);
|
|
}
|
|
}
|
|
|
|
static GESTimelineElement *
|
|
_paste (GESTimelineElement * element, GESTimelineElement * ref,
|
|
GstClockTime paste_position)
|
|
{
|
|
GList *tmp;
|
|
ChildMapping *map;
|
|
GESContainer *ncontainer =
|
|
GES_CONTAINER (ges_timeline_element_copy (element, FALSE));
|
|
GESContainer *self = GES_CONTAINER (element);
|
|
|
|
for (tmp = self->priv->copied_children; tmp; tmp = tmp->next) {
|
|
GESTimelineElement *nchild;
|
|
|
|
map = tmp->data;
|
|
nchild =
|
|
ges_timeline_element_paste (map->child,
|
|
paste_position - map->start_offset);
|
|
|
|
if (!nchild) {
|
|
while (ncontainer->children)
|
|
ges_container_remove (ncontainer, ncontainer->children->data);
|
|
|
|
g_object_unref (ncontainer);
|
|
return NULL;
|
|
}
|
|
|
|
/* for GESGroups, this may register the group on the timeline */
|
|
if (!ges_container_add (ncontainer, nchild))
|
|
GST_ERROR ("%" GES_FORMAT " could not add child %p while"
|
|
" copying, this should never happen", GES_ARGS (ncontainer), nchild);
|
|
}
|
|
|
|
return GES_TIMELINE_ELEMENT (ncontainer);
|
|
}
|
|
|
|
|
|
/******************************************
|
|
* *
|
|
* GObject virtual methods implementation *
|
|
* *
|
|
******************************************/
|
|
static void
|
|
_dispose (GObject * object)
|
|
{
|
|
GList *tmp;
|
|
GESContainer *self = GES_CONTAINER (object);
|
|
GList *children;
|
|
|
|
_ges_container_sort_children (self);
|
|
children = ges_container_get_children (self, FALSE);
|
|
|
|
for (tmp = g_list_last (children); tmp; tmp = tmp->prev)
|
|
ges_container_remove (self, tmp->data);
|
|
|
|
g_list_free_full (children, gst_object_unref);
|
|
self->children = NULL;
|
|
|
|
G_OBJECT_CLASS (ges_container_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
_finalize (GObject * object)
|
|
{
|
|
GESContainer *self = GES_CONTAINER (object);
|
|
|
|
g_list_free_full (self->priv->copied_children,
|
|
(GDestroyNotify) _free_mapping);
|
|
|
|
if (self->priv->mappings)
|
|
g_hash_table_destroy (self->priv->mappings);
|
|
|
|
G_OBJECT_CLASS (ges_container_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
_get_property (GObject * container, guint property_id,
|
|
GValue * value, GParamSpec * pspec)
|
|
{
|
|
GESContainer *tobj = GES_CONTAINER (container);
|
|
|
|
switch (property_id) {
|
|
case PROP_HEIGHT:
|
|
g_value_set_uint (value, tobj->height);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (container, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
_set_property (GObject * container, guint property_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
switch (property_id) {
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (container, property_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
ges_container_class_init (GESContainerClass * klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GESTimelineElementClass *element_class = GES_TIMELINE_ELEMENT_CLASS (klass);
|
|
|
|
GST_DEBUG_CATEGORY_INIT (ges_container_debug, "gescontainer",
|
|
GST_DEBUG_FG_YELLOW, "ges container");
|
|
|
|
object_class->get_property = _get_property;
|
|
object_class->set_property = _set_property;
|
|
object_class->dispose = _dispose;
|
|
object_class->finalize = _finalize;
|
|
|
|
/**
|
|
* GESContainer:height:
|
|
*
|
|
* The span of the container's children's #GESTimelineElement:priority
|
|
* values, which is the number of integers that lie between (inclusive)
|
|
* the minimum and maximum priorities found amongst the container's
|
|
* children (maximum - minimum + 1).
|
|
*/
|
|
properties[PROP_HEIGHT] = g_param_spec_uint ("height", "Height",
|
|
"The span of priorities this container occupies", 0, G_MAXUINT, 1,
|
|
G_PARAM_READABLE);
|
|
g_object_class_install_property (object_class, PROP_HEIGHT,
|
|
properties[PROP_HEIGHT]);
|
|
|
|
/**
|
|
* GESContainer::child-added:
|
|
* @container: The #GESContainer
|
|
* @element: The child that was added
|
|
*
|
|
* Will be emitted after a child is added to the container. Usually,
|
|
* you should connect with g_signal_connect_after() since the signal
|
|
* may be stopped internally.
|
|
*/
|
|
ges_container_signals[CHILD_ADDED_SIGNAL] =
|
|
g_signal_new ("child-added", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESContainerClass, child_added),
|
|
NULL, NULL, NULL, G_TYPE_NONE, 1, GES_TYPE_TIMELINE_ELEMENT);
|
|
|
|
/**
|
|
* GESContainer::child-removed:
|
|
* @container: The #GESContainer
|
|
* @element: The child that was removed
|
|
*
|
|
* Will be emitted after a child is removed from the container.
|
|
*/
|
|
ges_container_signals[CHILD_REMOVED_SIGNAL] =
|
|
g_signal_new ("child-removed", G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GESContainerClass, child_removed),
|
|
NULL, NULL, NULL, G_TYPE_NONE, 1, GES_TYPE_TIMELINE_ELEMENT);
|
|
|
|
|
|
element_class->set_start = _set_start;
|
|
element_class->set_duration = _set_duration;
|
|
element_class->lookup_child = _lookup_child;
|
|
element_class->get_track_types = _get_track_types;
|
|
element_class->paste = _paste;
|
|
element_class->deep_copy = _deep_copy;
|
|
|
|
/* No default implementations */
|
|
klass->remove_child = NULL;
|
|
klass->add_child = NULL;
|
|
klass->ungroup = NULL;
|
|
klass->group = NULL;
|
|
klass->grouping_priority = 0;
|
|
klass->edit = NULL;
|
|
}
|
|
|
|
static void
|
|
ges_container_init (GESContainer * self)
|
|
{
|
|
self->priv = ges_container_get_instance_private (self);
|
|
|
|
/* FIXME, check why default was GST_SECOND? (before the existend of
|
|
* ges-container)
|
|
*
|
|
* _DURATION (self) = GST_SECOND; */
|
|
self->height = 1; /* FIXME Why 1 and not 0? */
|
|
self->children = NULL;
|
|
|
|
self->priv->mappings = g_hash_table_new_full (g_direct_hash, g_direct_equal,
|
|
NULL, (GDestroyNotify) _free_mapping);
|
|
}
|
|
|
|
/**********************************************
|
|
* *
|
|
* Property notifications from Children *
|
|
* *
|
|
**********************************************/
|
|
|
|
static void
|
|
_update_start_duration (GESContainer * container, GESTimelineElement * child)
|
|
{
|
|
GList *tmp;
|
|
GstClockTime duration, end = 0, start = G_MAXUINT64;
|
|
gboolean was_being_edited = GES_TIMELINE_ELEMENT_BEING_EDITED (container);
|
|
|
|
if (!container->children) {
|
|
/* If we are now empty, keep the same duration and start. This works
|
|
* well for a clip. For a group, the duration should probably be set
|
|
* to 0, but it gets automatically removed from the timeline when it
|
|
* is emptied */
|
|
return;
|
|
}
|
|
|
|
GES_TIMELINE_ELEMENT_SET_BEING_EDITED (container);
|
|
|
|
for (tmp = container->children; tmp; tmp = tmp->next) {
|
|
start = MIN (start, _START (tmp->data));
|
|
end = MAX (end, _END (tmp->data));
|
|
}
|
|
|
|
if (end < start)
|
|
duration = 0;
|
|
else
|
|
duration = end - start;
|
|
|
|
if (start != _START (container) || duration != _DURATION (container)) {
|
|
GstClockTime prev_dur = _DURATION (container);
|
|
GstClockTime prev_start = _START (container);
|
|
|
|
_DURATION (container) = duration;
|
|
_START (container) = start;
|
|
|
|
GST_INFO ("%" GES_FORMAT " child %" GES_FORMAT " move made us move",
|
|
GES_ARGS (container), GES_ARGS (child));
|
|
|
|
if (prev_start != start)
|
|
g_object_notify (G_OBJECT (container), "start");
|
|
if (prev_dur != duration)
|
|
g_object_notify (G_OBJECT (container), "duration");
|
|
}
|
|
if (!was_being_edited)
|
|
GES_TIMELINE_ELEMENT_UNSET_BEING_EDITED (container);
|
|
|
|
g_hash_table_foreach (container->priv->mappings,
|
|
(GHFunc) _resync_position_offsets, container);
|
|
}
|
|
|
|
static void
|
|
_child_start_changed_cb (GESTimelineElement * child,
|
|
GParamSpec * arg G_GNUC_UNUSED, GESContainer * container)
|
|
{
|
|
ChildMapping *map;
|
|
|
|
GESContainerPrivate *priv = container->priv;
|
|
GESTimelineElement *element = GES_TIMELINE_ELEMENT (container);
|
|
GESChildrenControlMode mode = container->children_control_mode;
|
|
|
|
if (mode == GES_CHILDREN_IGNORE_NOTIFIES)
|
|
return;
|
|
|
|
if (GES_TIMELINE_ELEMENT_BEING_EDITED (child))
|
|
mode = GES_CHILDREN_UPDATE_ALL_VALUES;
|
|
|
|
map = g_hash_table_lookup (priv->mappings, child);
|
|
g_assert (map);
|
|
|
|
switch (mode) {
|
|
case GES_CHILDREN_UPDATE_ALL_VALUES:
|
|
_update_start_duration (container, child);
|
|
break;
|
|
case GES_CHILDREN_UPDATE_OFFSETS:
|
|
map->start_offset = _START (container) - _START (child);
|
|
break;
|
|
case GES_CHILDREN_UPDATE:
|
|
/* We update all the children calling our set_start method */
|
|
container->initiated_move = child;
|
|
_set_start0 (element, _START (child) + map->start_offset);
|
|
container->initiated_move = NULL;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
_child_duration_changed_cb (GESTimelineElement * child,
|
|
GParamSpec * arg G_GNUC_UNUSED, GESContainer * container)
|
|
{
|
|
ChildMapping *map;
|
|
|
|
GESContainerPrivate *priv = container->priv;
|
|
GESTimelineElement *element = GES_TIMELINE_ELEMENT (container);
|
|
GESChildrenControlMode mode = container->children_control_mode;
|
|
|
|
if (mode == GES_CHILDREN_IGNORE_NOTIFIES)
|
|
return;
|
|
|
|
if (GES_TIMELINE_ELEMENT_BEING_EDITED (child))
|
|
mode = GES_CHILDREN_UPDATE_ALL_VALUES;
|
|
|
|
map = g_hash_table_lookup (priv->mappings, child);
|
|
g_assert (map);
|
|
|
|
switch (mode) {
|
|
case GES_CHILDREN_UPDATE_ALL_VALUES:
|
|
_update_start_duration (container, child);
|
|
break;
|
|
case GES_CHILDREN_UPDATE_OFFSETS:
|
|
map->duration_offset = _DURATION (container) - _DURATION (child);
|
|
break;
|
|
case GES_CHILDREN_UPDATE:
|
|
/* We update all the children calling our set_duration method */
|
|
container->initiated_move = child;
|
|
/* FIXME: this is *not* the correct duration for a group!
|
|
* the ->set_duration method for GESGroup tries to hack around
|
|
* this by calling set_duration on itself to the actual value */
|
|
_set_duration0 (element, _DURATION (child) + map->duration_offset);
|
|
container->initiated_move = NULL;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/****************************************************
|
|
* *
|
|
* Internal methods implementation *
|
|
* *
|
|
****************************************************/
|
|
|
|
void
|
|
_ges_container_sort_children (GESContainer * container)
|
|
{
|
|
container->children = g_list_sort (container->children,
|
|
(GCompareFunc) element_start_compare);
|
|
}
|
|
|
|
void
|
|
_ges_container_set_height (GESContainer * container, guint32 height)
|
|
{
|
|
if (container->height != height) {
|
|
container->height = height;
|
|
GST_DEBUG_OBJECT (container, "Updating height %i", container->height);
|
|
g_object_notify (G_OBJECT (container), "height");
|
|
}
|
|
}
|
|
|
|
/**********************************************
|
|
* *
|
|
* API implementation *
|
|
* *
|
|
**********************************************/
|
|
|
|
/**
|
|
* ges_container_add:
|
|
* @container: A #GESContainer
|
|
* @child: The element to add as a child
|
|
*
|
|
* Adds a timeline element to the container. The element will now be a
|
|
* child of the container (and the container will be the
|
|
* #GESTimelineElement:parent of the added element), which means that it
|
|
* is now controlled by the container. This may change the properties of
|
|
* the child or the container, depending on the subclass.
|
|
*
|
|
* Additionally, the children properties of the newly added element will
|
|
* be shared with the container, meaning they can also be read and set
|
|
* using ges_timeline_element_get_child_property() and
|
|
* ges_timeline_element_set_child_property() on the container.
|
|
*
|
|
* Returns: %TRUE if @child was successfully added to @container.
|
|
*/
|
|
gboolean
|
|
ges_container_add (GESContainer * container, GESTimelineElement * child)
|
|
{
|
|
ChildMapping *mapping;
|
|
gboolean ret = FALSE;
|
|
GESContainerClass *class;
|
|
GList *current_children, *tmp;
|
|
GESContainerPrivate *priv;
|
|
|
|
g_return_val_if_fail (GES_IS_CONTAINER (container), FALSE);
|
|
g_return_val_if_fail (GES_IS_TIMELINE_ELEMENT (child), FALSE);
|
|
g_return_val_if_fail (GES_TIMELINE_ELEMENT_PARENT (child) == NULL, FALSE);
|
|
|
|
class = GES_CONTAINER_GET_CLASS (container);
|
|
priv = container->priv;
|
|
|
|
GST_DEBUG_OBJECT (container, "adding timeline element %" GST_PTR_FORMAT,
|
|
child);
|
|
|
|
/* freeze all notifies */
|
|
g_object_freeze_notify (G_OBJECT (container));
|
|
/* copy to use at end, since container->children may have child
|
|
* added to it */
|
|
current_children = g_list_copy_deep (container->children,
|
|
(GCopyFunc) gst_object_ref, NULL);
|
|
for (tmp = current_children; tmp; tmp = tmp->next)
|
|
g_object_freeze_notify (G_OBJECT (tmp->data));
|
|
g_object_freeze_notify (G_OBJECT (child));
|
|
gst_object_ref_sink (child);
|
|
|
|
if (class->add_child) {
|
|
if (class->add_child (container, child) == FALSE) {
|
|
GST_WARNING_OBJECT (container, "Error adding child %p", child);
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
mapping = g_slice_new0 (ChildMapping);
|
|
mapping->child = gst_object_ref (child);
|
|
g_hash_table_insert (priv->mappings, child, mapping);
|
|
container->children = g_list_append (container->children, child);
|
|
|
|
/* Listen to all property changes */
|
|
mapping->start_notifyid =
|
|
g_signal_connect (G_OBJECT (child), "notify::start",
|
|
G_CALLBACK (_child_start_changed_cb), container);
|
|
mapping->duration_notifyid =
|
|
g_signal_connect (G_OBJECT (child), "notify::duration",
|
|
G_CALLBACK (_child_duration_changed_cb), container);
|
|
|
|
if (ges_timeline_element_set_parent (child, GES_TIMELINE_ELEMENT (container))
|
|
== FALSE) {
|
|
if (class->remove_child)
|
|
class->remove_child (container, child);
|
|
|
|
g_hash_table_remove (priv->mappings, child);
|
|
container->children = g_list_remove (container->children, child);
|
|
|
|
goto done;
|
|
}
|
|
|
|
_update_start_duration (container, child);
|
|
_ges_container_sort_children (container);
|
|
|
|
_ges_container_add_child_properties (container, child);
|
|
mapping->child_property_added_notifyid =
|
|
g_signal_connect (G_OBJECT (child), "child-property-added",
|
|
G_CALLBACK (_add_childs_child_property), container);
|
|
mapping->child_property_removed_notifyid =
|
|
g_signal_connect (G_OBJECT (child), "child-property-removed",
|
|
G_CALLBACK (_remove_childs_child_property), container);
|
|
|
|
priv->adding_children = g_list_prepend (priv->adding_children, child);
|
|
g_signal_emit (container, ges_container_signals[CHILD_ADDED_SIGNAL], 0,
|
|
child);
|
|
priv->adding_children = g_list_remove (priv->adding_children, child);
|
|
|
|
ret = TRUE;
|
|
|
|
done:
|
|
/* thaw all notifies */
|
|
/* Ignore notifies for the start and duration since the child should
|
|
* already be correctly set up */
|
|
container->children_control_mode = GES_CHILDREN_IGNORE_NOTIFIES;
|
|
g_object_thaw_notify (G_OBJECT (container));
|
|
for (tmp = current_children; tmp; tmp = tmp->next)
|
|
g_object_thaw_notify (G_OBJECT (tmp->data));
|
|
g_object_thaw_notify (G_OBJECT (child));
|
|
g_list_free_full (current_children, gst_object_unref);
|
|
gst_object_unref (child);
|
|
container->children_control_mode = GES_CHILDREN_UPDATE;
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* ges_container_remove:
|
|
* @container: A #GESContainer
|
|
* @child: The child to remove
|
|
*
|
|
* Removes a timeline element from the container. The element will no
|
|
* longer be controlled by the container.
|
|
*
|
|
* Returns: %TRUE if @child was successfully removed from @container.
|
|
*/
|
|
gboolean
|
|
ges_container_remove (GESContainer * container, GESTimelineElement * child)
|
|
{
|
|
GESContainerClass *klass;
|
|
GESContainerPrivate *priv;
|
|
GList *current_children, *tmp;
|
|
gboolean ret = FALSE;
|
|
|
|
g_return_val_if_fail (GES_IS_CONTAINER (container), FALSE);
|
|
g_return_val_if_fail (GES_IS_TIMELINE_ELEMENT (child), FALSE);
|
|
|
|
GST_DEBUG_OBJECT (container, "removing child: %" GST_PTR_FORMAT, child);
|
|
|
|
klass = GES_CONTAINER_GET_CLASS (container);
|
|
priv = container->priv;
|
|
|
|
if (!(g_hash_table_lookup (priv->mappings, child))) {
|
|
GST_WARNING_OBJECT (container, "Element isn't controlled by this "
|
|
"container");
|
|
return FALSE;
|
|
}
|
|
|
|
/* ref the container since it might be destroyed when the child is
|
|
* removed! (see GESGroup ->child_removed) */
|
|
gst_object_ref (container);
|
|
/* freeze all notifies */
|
|
g_object_freeze_notify (G_OBJECT (container));
|
|
/* copy to use at end, since container->children may have child
|
|
* removed from it */
|
|
current_children = g_list_copy_deep (container->children,
|
|
(GCopyFunc) gst_object_ref, NULL);
|
|
for (tmp = current_children; tmp; tmp = tmp->next)
|
|
g_object_freeze_notify (G_OBJECT (tmp->data));
|
|
|
|
|
|
if (klass->remove_child) {
|
|
if (klass->remove_child (container, child) == FALSE)
|
|
goto done;
|
|
}
|
|
|
|
container->children = g_list_remove (container->children, child);
|
|
g_hash_table_remove (priv->mappings, child);
|
|
|
|
_ges_container_remove_child_properties (container, child);
|
|
|
|
if (!g_list_find (container->priv->adding_children, child)) {
|
|
g_signal_emit (container, ges_container_signals[CHILD_REMOVED_SIGNAL], 0,
|
|
child);
|
|
} else {
|
|
GESContainerClass *klass = GES_CONTAINER_GET_CLASS (container);
|
|
|
|
if (klass->child_removed)
|
|
klass->child_removed (container, child);
|
|
|
|
GST_INFO_OBJECT (container,
|
|
"Not emitting 'child-removed' signal as child"
|
|
" removal happend during 'child-added' signal emission");
|
|
}
|
|
|
|
_update_start_duration (container, child);
|
|
|
|
ret = TRUE;
|
|
|
|
done:
|
|
/* thaw all notifies */
|
|
g_object_thaw_notify (G_OBJECT (container));
|
|
for (tmp = current_children; tmp; tmp = tmp->next)
|
|
g_object_thaw_notify (G_OBJECT (tmp->data));
|
|
g_list_free_full (current_children, gst_object_unref);
|
|
|
|
gst_object_unref (container);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
_get_children_recursively (GESContainer * container, GList ** children)
|
|
{
|
|
GList *tmp;
|
|
|
|
*children =
|
|
g_list_concat (*children, g_list_copy_deep (container->children,
|
|
(GCopyFunc) gst_object_ref, NULL));
|
|
|
|
for (tmp = container->children; tmp; tmp = tmp->next) {
|
|
GESTimelineElement *element = tmp->data;
|
|
|
|
if (GES_IS_CONTAINER (element))
|
|
_get_children_recursively (tmp->data, children);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ges_container_get_children:
|
|
* @container: A #GESContainer
|
|
* @recursive: Whether to recursively get children in @container
|
|
*
|
|
* Get the list of timeline elements contained in the container. If
|
|
* @recursive is %TRUE, and the container contains other containers as
|
|
* children, then their children will be added to the list, in addition to
|
|
* themselves, and so on.
|
|
*
|
|
* Returns: (transfer full) (element-type GESTimelineElement): The list of
|
|
* #GESTimelineElement-s contained in @container.
|
|
*/
|
|
GList *
|
|
ges_container_get_children (GESContainer * container, gboolean recursive)
|
|
{
|
|
GList *children = NULL;
|
|
|
|
g_return_val_if_fail (GES_IS_CONTAINER (container), NULL);
|
|
|
|
if (!recursive)
|
|
return g_list_copy_deep (container->children, (GCopyFunc) gst_object_ref,
|
|
NULL);
|
|
|
|
_get_children_recursively (container, &children);
|
|
return children;
|
|
}
|
|
|
|
/**
|
|
* ges_container_ungroup:
|
|
* @container: (transfer full): The container to ungroup
|
|
* @recursive: Whether to recursively ungroup @container
|
|
*
|
|
* Ungroups the container by splitting it into several containers
|
|
* containing various children of the original. The rules for how the
|
|
* container splits depends on the subclass. A #GESGroup will simply split
|
|
* into its children. A #GESClip will split into one #GESClip per
|
|
* #GESTrackType it overlaps with (so an audio-video clip will split into
|
|
* an audio clip and a video clip), where each clip contains all the
|
|
* #GESTrackElement-s from the original clip with a matching
|
|
* #GESTrackElement:track-type.
|
|
*
|
|
* If @recursive is %TRUE, and the container contains other containers as
|
|
* children, then they will also be ungrouped, and so on.
|
|
*
|
|
* Returns: (transfer full) (element-type GESContainer): The list of
|
|
* new #GESContainer-s created from the splitting of @container.
|
|
*/
|
|
GList *
|
|
ges_container_ungroup (GESContainer * container, gboolean recursive)
|
|
{
|
|
GESContainerClass *klass;
|
|
|
|
g_return_val_if_fail (GES_IS_CONTAINER (container), NULL);
|
|
|
|
GST_DEBUG_OBJECT (container, "Ungrouping container %s recursively",
|
|
recursive ? "" : "not");
|
|
|
|
klass = GES_CONTAINER_GET_CLASS (container);
|
|
if (klass->ungroup == NULL) {
|
|
GST_INFO_OBJECT (container, "No ungoup virtual method, doint nothing");
|
|
return NULL;
|
|
}
|
|
|
|
return klass->ungroup (container, recursive);
|
|
}
|
|
|
|
/**
|
|
* ges_container_group:
|
|
* @containers: (transfer none)(element-type GESContainer) (allow-none):
|
|
* The #GESContainer-s to group
|
|
*
|
|
* Groups the containers into a single container by merging them. The
|
|
* containers must all belong to the same #GESTimelineElement:timeline.
|
|
*
|
|
* If the elements are all #GESClip-s then this method will attempt to
|
|
* combine them all into a single #GESClip. This should succeed if they:
|
|
* share the same #GESTimelineElement:start, #GESTimelineElement:duration
|
|
* and #GESTimelineElement:in-point; exist in the same layer; and all of
|
|
* the sources share the same #GESAsset. If this fails, or one of the
|
|
* elements is not a #GESClip, this method will try to create a #GESGroup
|
|
* instead.
|
|
*
|
|
* Returns: (transfer floating): The container created by merging
|
|
* @containers, or %NULL if they could not be merged into a single
|
|
* container.
|
|
*/
|
|
GESContainer *
|
|
ges_container_group (GList * containers)
|
|
{
|
|
GList *tmp;
|
|
guint n_children;
|
|
GType *children_types;
|
|
GESTimelineElement *element;
|
|
GObjectClass *clip_class;
|
|
|
|
guint i = 0;
|
|
GESContainer *ret = NULL;
|
|
GESTimeline *timeline = NULL;
|
|
|
|
if (containers) {
|
|
element = GES_TIMELINE_ELEMENT (containers->data);
|
|
timeline = GES_TIMELINE_ELEMENT_TIMELINE (element);
|
|
g_return_val_if_fail (timeline, NULL);
|
|
}
|
|
|
|
if (g_list_length (containers) == 1) {
|
|
/* FIXME: Should return a floating **copy**. API specifies that the
|
|
* returned element is created. So users might expect to be able to
|
|
* freely dispose of the list, without the risk of the returned
|
|
* element being freed as well.
|
|
* TODO 2.0: (transfer full) would have been better */
|
|
return containers->data;
|
|
}
|
|
|
|
for (tmp = containers; tmp; tmp = tmp->next) {
|
|
g_return_val_if_fail (GES_IS_CONTAINER (tmp->data), NULL);
|
|
g_return_val_if_fail (GES_TIMELINE_ELEMENT_PARENT (tmp->data) == NULL,
|
|
NULL);
|
|
g_return_val_if_fail (GES_TIMELINE_ELEMENT_TIMELINE (tmp->data) == timeline,
|
|
NULL);
|
|
}
|
|
|
|
/* FIXME: how can user sub-classes interact with this if
|
|
* ->grouping_priority is private? */
|
|
children_types = g_type_children (GES_TYPE_CONTAINER, &n_children);
|
|
g_qsort_with_data (children_types, n_children, sizeof (GType),
|
|
(GCompareDataFunc) compare_grouping_prio, NULL);
|
|
|
|
for (i = 0; i < n_children; i++) {
|
|
clip_class = g_type_class_peek (children_types[i]);
|
|
/* FIXME: handle NULL ->group */
|
|
ret = GES_CONTAINER_CLASS (clip_class)->group (containers);
|
|
|
|
if (ret)
|
|
break;
|
|
}
|
|
|
|
g_free (children_types);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* ges_container_edit:
|
|
* @container: The #GESContainer to edit
|
|
* @layers: (element-type GESLayer) (nullable): A whitelist of layers
|
|
* where the edit can be performed, %NULL allows all layers in the
|
|
* timeline
|
|
* @new_layer_priority: The priority/index of the layer @container should
|
|
* be moved to. -1 means no move
|
|
* @mode: The edit mode
|
|
* @edge: The edge of @container where the edit should occur
|
|
* @position: The edit position: a new location for the edge of @container
|
|
* (in nanoseconds)
|
|
*
|
|
* Edits the container within its timeline.
|
|
*
|
|
* Returns: %TRUE if the edit of @container completed, %FALSE on failure.
|
|
*
|
|
* Deprecated: 1.18: use #ges_timeline_element_edit instead.
|
|
*/
|
|
gboolean
|
|
ges_container_edit (GESContainer * container, GList * layers,
|
|
gint new_layer_priority, GESEditMode mode, GESEdge edge, guint64 position)
|
|
{
|
|
g_return_val_if_fail (GES_IS_CONTAINER (container), FALSE);
|
|
|
|
return ges_timeline_element_edit (GES_TIMELINE_ELEMENT (container),
|
|
layers, new_layer_priority, mode, edge, position);
|
|
}
|