ges: Add a way to set layer activeness by track

a.k.a muting layers.

Adding unit tests and making sure serialization works properly
This commit is contained in:
Thibault Saunier 2020-03-06 18:56:52 -03:00
parent 7ccc7aa7ca
commit f9f30c4ced
15 changed files with 707 additions and 59 deletions

View file

@ -943,7 +943,7 @@ ges_base_xml_formatter_set_timeline_properties (GESBaseXmlFormatter * self,
void
ges_base_xml_formatter_add_layer (GESBaseXmlFormatter * self,
GType extractable_type, guint priority, GstStructure * properties,
const gchar * metadatas, GError ** error)
const gchar * metadatas, gchar ** deactivated_tracks, GError ** error)
{
LayerEntry *entry;
GESAsset *asset;
@ -967,10 +967,11 @@ ges_base_xml_formatter_add_layer (GESBaseXmlFormatter * self,
G_MARKUP_ERROR_INVALID_CONTENT,
"Layer type %s could not be created'",
g_type_name (extractable_type));
return;
}
return;
}
layer = GES_LAYER (ges_asset_extract (asset, error));
gst_object_unref (asset);
}
ges_layer_set_priority (layer, priority);
@ -988,6 +989,27 @@ ges_base_xml_formatter_add_layer (GESBaseXmlFormatter * self,
ges_meta_container_add_metas_from_string (GES_META_CONTAINER (layer),
metadatas);
if (deactivated_tracks) {
gint i;
GList *tracks = NULL;
for (i = 0; deactivated_tracks[i] && deactivated_tracks[i][0] != '\0'; i++) {
GESTrack *track =
g_hash_table_lookup (priv->tracks, deactivated_tracks[i]);
if (!track) {
GST_ERROR_OBJECT (self,
"Unknown deactivated track: %s", deactivated_tracks[i]);
continue;
}
tracks = g_list_append (tracks, track);
}
ges_layer_set_active_for_tracks (layer, FALSE, tracks);
g_list_free (tracks);
}
entry = g_slice_new0 (LayerEntry);
entry->layer = gst_object_ref (layer);
entry->auto_trans = auto_transition;

View file

@ -291,6 +291,7 @@ G_GNUC_INTERNAL void ges_base_xml_formatter_add_layer (GESBaseXmlForma
guint priority,
GstStructure *properties,
const gchar *metadatas,
gchar **deactivated_tracks,
GError **error);
G_GNUC_INTERNAL void ges_base_xml_formatter_add_track (GESBaseXmlFormatter *self,
GESTrackType track_type,
@ -418,6 +419,7 @@ G_GNUC_INTERNAL void layer_set_priority (GESLayer * layer, guint p
G_GNUC_INTERNAL gboolean ges_track_element_set_track (GESTrackElement * object, GESTrack * track);
G_GNUC_INTERNAL void ges_track_element_copy_properties (GESTimelineElement * element,
GESTimelineElement * elementcopy);
G_GNUC_INTERNAL void ges_track_element_set_layer_active (GESTrackElement *element, gboolean active);
G_GNUC_INTERNAL void ges_track_element_copy_bindings (GESTrackElement *element,
GESTrackElement *new_element,

View file

@ -65,6 +65,8 @@ struct _GESLayerPrivate
guint32 priority; /* The priority of the layer within the
* containing timeline */
gboolean auto_transition;
GHashTable *tracks_activness;
};
typedef struct
@ -73,6 +75,43 @@ typedef struct
GESLayer *layer;
} NewAssetUData;
typedef struct
{
GESTrack *track;
GESLayer *layer;
gboolean active;
gboolean track_disposed;
} LayerActivnessData;
static void
_track_disposed_cb (LayerActivnessData * data, GObject * disposed_track)
{
data->track_disposed = TRUE;
g_hash_table_remove (data->layer->priv->tracks_activness, data->track);
}
static void
layer_activness_data_free (LayerActivnessData * data)
{
if (!data->track_disposed)
g_object_weak_unref ((GObject *) data->track,
(GWeakNotify) _track_disposed_cb, data);
g_free (data);
}
static LayerActivnessData *
layer_activness_data_new (GESTrack * track, GESLayer * layer, gboolean active)
{
LayerActivnessData *data = g_new0 (LayerActivnessData, 1);
data->layer = layer;
data->track = track;
data->active = active;
g_object_weak_ref (G_OBJECT (track), (GWeakNotify) _track_disposed_cb, data);
return data;
}
enum
{
PROP_0,
@ -85,6 +124,7 @@ enum
{
OBJECT_ADDED,
OBJECT_REMOVED,
ACTIVE_CHANGED,
LAST_SIGNAL
};
@ -145,6 +185,8 @@ ges_layer_dispose (GObject * object)
while (priv->clips_start)
ges_layer_remove_clip (layer, (GESClip *) priv->clips_start->data);
g_clear_pointer (&layer->priv->tracks_activness, g_hash_table_unref);
G_OBJECT_CLASS (ges_layer_parent_class)->dispose (object);
}
@ -245,6 +287,21 @@ ges_layer_class_init (GESLayerClass * klass)
g_signal_new ("clip-removed", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GESLayerClass,
object_removed), NULL, NULL, NULL, G_TYPE_NONE, 1, GES_TYPE_CLIP);
/**
* GESLayer::active-changed:
* @layer: The #GESLayer
* @active: Whether @layer has been made active or de-active in the @tracks
* @tracks: (element-type GESTrack) (transfer none): A list of #GESTrack
* which have been activated or deactivated
*
* Will be emitted whenever the layer is activated or deactivated
* for some #GESTrack. See ges_layer_set_active_for_tracks().
*/
ges_layer_signals[ACTIVE_CHANGED] =
g_signal_new ("active-changed", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2,
G_TYPE_BOOLEAN, G_TYPE_PTR_ARRAY);
}
static void
@ -257,6 +314,10 @@ ges_layer_init (GESLayer * self)
self->min_nle_priority = MIN_NLE_PRIO;
self->max_nle_priority = LAYER_HEIGHT + MIN_NLE_PRIO;
self->priv->tracks_activness =
g_hash_table_new_full (g_direct_hash, g_direct_equal,
NULL, (GDestroyNotify) layer_activness_data_free);
_register_metas (self);
}
@ -420,6 +481,7 @@ ges_layer_remove_clip_internal (GESLayer * layer, GESClip * clip,
gboolean emit_removed)
{
GESLayer *current_layer;
GList *tmp;
GST_DEBUG ("layer:%p, clip:%p", layer, clip);
@ -448,6 +510,9 @@ ges_layer_remove_clip_internal (GESLayer * layer, GESClip * clip,
if (layer->timeline)
ges_timeline_element_set_timeline (GES_TIMELINE_ELEMENT (clip), NULL);
for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next)
ges_track_element_set_layer_active (tmp->data, TRUE);
/* Remove our reference to the clip */
gst_object_unref (clip);
@ -618,6 +683,7 @@ ges_layer_is_empty (GESLayer * layer)
gboolean
ges_layer_add_clip (GESLayer * layer, GESClip * clip)
{
GList *tmp;
GESAsset *asset;
GESLayerPrivate *priv;
GESLayer *current_layer;
@ -723,6 +789,14 @@ ges_layer_add_clip (GESLayer * layer, GESClip * clip)
return FALSE;
}
for (tmp = GES_CONTAINER_CHILDREN (clip); tmp; tmp = tmp->next) {
GESTrack *track = ges_track_element_get_track (tmp->data);
if (track)
ges_track_element_set_layer_active (tmp->data,
ges_layer_get_active_for_track (layer, track));
}
return TRUE;
}
@ -879,3 +953,87 @@ ges_layer_get_clips_in_interval (GESLayer * layer, GstClockTime start,
}
return intersecting_clips;
}
/**
* ges_layer_get_active_for_track:
* @layer: The #GESLayer
* @track: The #GESTrack to check if @layer is currently active for
*
* Gets whether the layer is active for the given track. See
* ges_layer_set_active_for_tracks().
*
* Returns: %TRUE if @layer is active for @track, or %FALSE otherwise.
*/
gboolean
ges_layer_get_active_for_track (GESLayer * layer, GESTrack * track)
{
LayerActivnessData *d;
g_return_val_if_fail (GES_IS_LAYER (layer), FALSE);
g_return_val_if_fail (GES_IS_TRACK (track), FALSE);
g_return_val_if_fail (layer->timeline == ges_track_get_timeline (track),
FALSE);
d = g_hash_table_lookup (layer->priv->tracks_activness, track);
return d ? d->active : TRUE;
}
/**
* ges_layer_set_active_for_tracks:
* @layer: The #GESLayer
* @active: Whether elements in @tracks should be active or not
* @tracks: (transfer none) (element-type GESTrack) (allow-none): The list of
* tracks @layer should be (de-)active in, or %NULL to include all the tracks
* in the @layer's timeline
*
* Activate or deactivate track elements in @tracks (or in all tracks if @tracks
* is %NULL).
*
* When a layer is deactivated for a track, all the #GESTrackElement-s in
* the track that belong to a #GESClip in the layer will no longer be
* active in the track, regardless of their individual
* #GESTrackElement:active value.
*
* Note that by default a layer will be active for all of its
* timeline's tracks.
*
* Returns: %TRUE if the operation worked %FALSE otherwise.
*/
gboolean
ges_layer_set_active_for_tracks (GESLayer * layer, gboolean active,
GList * tracks)
{
GList *tmp, *owned_tracks = NULL;
GPtrArray *changed_tracks = NULL;
g_return_val_if_fail (GES_IS_LAYER (layer), FALSE);
if (!tracks && layer->timeline)
owned_tracks = tracks = ges_timeline_get_tracks (layer->timeline);
for (tmp = tracks; tmp; tmp = tmp->next) {
GESTrack *track = tmp->data;
/* Handle setting timeline later */
g_return_val_if_fail (layer->timeline == ges_track_get_timeline (track),
FALSE);
if (ges_layer_get_active_for_track (layer, track) != active) {
if (changed_tracks == NULL)
changed_tracks = g_ptr_array_new ();
g_ptr_array_add (changed_tracks, track);
}
g_hash_table_insert (layer->priv->tracks_activness, track,
layer_activness_data_new (track, layer, active));
}
if (changed_tracks) {
g_signal_emit (layer, ges_layer_signals[ACTIVE_CHANGED], 0, active,
changed_tracks);
g_ptr_array_unref (changed_tracks);
}
g_list_free_full (owned_tracks, gst_object_unref);
return TRUE;
}

View file

@ -121,5 +121,11 @@ GES_API
GList* ges_layer_get_clips (GESLayer * layer);
GES_API
GstClockTime ges_layer_get_duration (GESLayer *layer);
GES_API
gboolean ges_layer_set_active_for_tracks(GESLayer *layer, gboolean active,
GList *tracks);
GES_API gboolean ges_layer_get_active_for_track(GESLayer *layer,
GESTrack *track);
G_END_DECLS

View file

@ -1333,3 +1333,30 @@ timeline_tree_get_duration (GNode * root)
return duration;
}
static gboolean
reset_layer_activness (GNode * node, GESLayer * layer)
{
GESTrack *track;
if (!GES_IS_TRACK_ELEMENT (node->data))
return FALSE;
track = ges_track_element_get_track (node->data);
if (!track || (ges_timeline_element_get_layer_priority (node->data) !=
ges_layer_get_priority (layer)))
return FALSE;
ges_track_element_set_layer_active (node->data,
ges_layer_get_active_for_track (layer, track));
return FALSE;
}
void
timeline_tree_reset_layer_active (GNode * root, GESLayer * layer)
{
g_node_traverse (root, G_PRE_ORDER, G_TRAVERSE_LEAFS, -1,
(GNodeTraverseFunc) reset_layer_activness, layer);
}

View file

@ -73,4 +73,6 @@ ges_timeline_find_auto_transition (GESTimeline * timeline, GESTrackEleme
void
timeline_update_duration (GESTimeline * timeline);
void timeline_tree_reset_layer_active (GNode *root, GESLayer *layer);
void timeline_tree_init_debug (void);

View file

@ -1350,6 +1350,13 @@ add_object_to_tracks (GESTimeline * timeline, GESClip * clip, GESTrack * track)
UNLOCK_DYN (timeline);
}
static void
layer_active_changed_cb (GESLayer * layer, gboolean active G_GNUC_UNUSED,
GPtrArray * tracks G_GNUC_UNUSED, GESTimeline * timeline)
{
timeline_tree_reset_layer_active (timeline->priv->tree, layer);
}
static void
layer_auto_transition_changed_cb (GESLayer * layer,
GParamSpec * arg G_GNUC_UNUSED, GESTimeline * timeline)
@ -2050,6 +2057,8 @@ ges_timeline_add_layer (GESTimeline * timeline, GESLayer * layer)
G_CALLBACK (layer_priority_changed_cb), timeline);
g_signal_connect (layer, "notify::auto-transition",
G_CALLBACK (layer_auto_transition_changed_cb), timeline);
g_signal_connect_after (layer, "active-changed",
G_CALLBACK (layer_active_changed_cb), timeline);
GST_DEBUG ("Done adding layer, emitting 'layer-added' signal");
g_signal_emit (timeline, ges_timeline_signals[LAYER_ADDED], 0, layer);
@ -2111,6 +2120,8 @@ ges_timeline_remove_layer (GESTimeline * timeline, GESLayer * layer)
timeline);
g_signal_handlers_disconnect_by_func (layer,
layer_auto_transition_changed_cb, timeline);
g_signal_handlers_disconnect_by_func (layer, layer_active_changed_cb,
timeline);
timeline->layers = g_list_remove (timeline->layers, layer);
ges_layer_set_timeline (layer, NULL);

View file

@ -63,6 +63,7 @@ struct _GESTrackElementPrivate
gboolean locked; /* If TRUE, then moves in sync with its controlling
* GESClip */
gboolean layer_active;
GHashTable *bindings_hashtable; /* We need this if we want to be able to serialize
and deserialize keyframes */
@ -302,7 +303,7 @@ ges_track_element_constructed (GObject * gobject)
"inpoint", GES_TIMELINE_ELEMENT_INPOINT (object),
"duration", GES_TIMELINE_ELEMENT_DURATION (object),
"priority", GES_TIMELINE_ELEMENT_PRIORITY (object),
"active", object->active, NULL);
"active", object->active & object->priv->layer_active, NULL);
media_duration_factor =
ges_timeline_element_get_media_duration_factor (GES_TIMELINE_ELEMENT
@ -467,6 +468,7 @@ ges_track_element_init (GESTrackElement * self)
GES_TIMELINE_ELEMENT_DURATION (self) = GST_SECOND;
GES_TIMELINE_ELEMENT_PRIORITY (self) = 0;
self->active = TRUE;
self->priv->layer_active = TRUE;
priv->bindings_hashtable = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, NULL);
@ -740,7 +742,8 @@ ges_track_element_set_active (GESTrackElement * object, gboolean active)
if (G_UNLIKELY (active == object->active))
return FALSE;
g_object_set (object->priv->nleobject, "active", active, NULL);
g_object_set (object->priv->nleobject, "active",
active & object->priv->layer_active, NULL);
object->active = active;
if (GES_TRACK_ELEMENT_GET_CLASS (object)->active_changed)
@ -1050,6 +1053,17 @@ ges_track_element_set_track (GESTrackElement * object, GESTrack * track)
return ret;
}
void
ges_track_element_set_layer_active (GESTrackElement * element, gboolean active)
{
if (element->priv->layer_active == active)
return;
element->priv->layer_active = active;
g_object_set (element->priv->nleobject, "active", active & element->active,
NULL);
}
/**
* ges_track_element_get_all_control_bindings
* @trackelement: A #GESTrackElement

View file

@ -232,6 +232,17 @@ update_gaps (GESTrack * track)
if (!ges_track_element_is_active (trackelement))
continue;
if (priv->timeline) {
guint32 layer_prio = GES_TIMELINE_ELEMENT_LAYER_PRIORITY (trackelement);
if (layer_prio != GES_TIMELINE_ELEMENT_NO_LAYER_PRIORITY) {
GESLayer *layer = g_list_nth_data (priv->timeline->layers, layer_prio);
if (!ges_layer_get_active_for_track (layer, track))
continue;
}
}
start = _START (trackelement);
end = start + _DURATION (trackelement);

View file

@ -1066,6 +1066,74 @@ done:
return res;
}
static gint
set_layer_active (GstValidateScenario * scenario, GstValidateAction * action)
{
gboolean active;
gint i, layer_prio;
GESLayer *layer;
GList *tracks = NULL;
GstValidateExecuteActionReturn res = GST_VALIDATE_EXECUTE_ACTION_OK;
gchar **track_names =
gst_validate_utils_get_strv (action->structure, "tracks");
DECLARE_AND_GET_TIMELINE (scenario, action);
for (i = 0; track_names[i]; i++) {
GESTrack *track =
(GESTrack *) gst_bin_get_by_name (GST_BIN (timeline), track_names[i]);
if (!track) {
GST_VALIDATE_REPORT_ACTION (scenario, action,
SCENARIO_ACTION_EXECUTION_ERROR,
"Could not find track %s", track_names[i]);
res = GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED;
goto done;
}
tracks = g_list_prepend (tracks, track);
}
if (!gst_structure_get_int (action->structure, "layer-priority", &layer_prio)) {
GST_VALIDATE_REPORT_ACTION (scenario, action,
SCENARIO_ACTION_EXECUTION_ERROR,
"Could not find layer from %" GST_PTR_FORMAT, action->structure);
res = GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED;
goto done;
}
if (!(layer = g_list_nth_data (timeline->layers, layer_prio))) {
GST_VALIDATE_REPORT_ACTION (scenario, action,
SCENARIO_ACTION_EXECUTION_ERROR, "Could not find layer %d", layer_prio);
res = GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED;
goto done;
}
if (!gst_structure_get_boolean (action->structure, "active", &active)) {
GST_VALIDATE_REPORT_ACTION (scenario, action,
SCENARIO_ACTION_EXECUTION_ERROR,
"Could not find 'active' boolean in %" GST_PTR_FORMAT,
action->structure);
res = GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED;
goto done;
}
if (!ges_layer_set_active_for_tracks (layer, active, tracks)) {
GST_VALIDATE_REPORT_ACTION (scenario, action,
SCENARIO_ACTION_EXECUTION_ERROR,
"Could not set active for track defined in %" GST_PTR_FORMAT,
action->structure);
res = GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED;
goto done;
}
done:
g_strfreev (track_names);
gst_object_unref (timeline);
g_list_free_full (tracks, gst_object_unref);
return res;
}
#endif
gboolean
@ -1404,6 +1472,43 @@ ges_validate_register_action_types (void)
{NULL}
}, "Allows to change child property of an object", GST_VALIDATE_ACTION_TYPE_NONE);
gst_validate_register_action_type ("set-layer-active", "ges", set_layer_active,
(GstValidateActionParameter []) {
{
.name = "layer-priority",
.description = "The priority of the layer to set activness on",
.types = "gint",
.mandatory = TRUE,
},
{
.name = "active",
.description = "The activness of the layer",
.types = "gboolean",
.mandatory = TRUE,
},
{
.name = "tracks",
.description = "tracks",
.types = "{string, }",
.mandatory = FALSE,
},
{NULL}
}, "Set activness of a layer (on optional tracks).",
GST_VALIDATE_ACTION_TYPE_NONE);
gst_validate_register_action_type ("set-ges-properties", "ges", set_or_check_properties,
(GstValidateActionParameter []) {
{
.name = "element-name",
.description = "The name of the element on which to set properties",
.types = "string",
.mandatory = TRUE,
},
{NULL}
}, "Set `element-name` properties values defined by the"
" fields in the following format: `property_name=expected-value`",
GST_VALIDATE_ACTION_TYPE_NONE);
gst_validate_register_action_type ("check-ges-properties", "ges", set_or_check_properties,
(GstValidateActionParameter []) {
{

View file

@ -35,8 +35,8 @@
#define parent_class ges_xml_formatter_parent_class
#define API_VERSION 0
#define MINOR_VERSION 6
#define VERSION 0.5
#define MINOR_VERSION 7
#define VERSION 0.7
#define COLLECT_STR_OPT (G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL)
@ -471,13 +471,16 @@ _parse_layer (GMarkupParseContext * context, const gchar * element_name,
guint priority;
GType extractable_type = G_TYPE_NONE;
const gchar *metadatas = NULL, *properties = NULL, *strprio = NULL,
*extractable_type_name;
*extractable_type_name, *deactivated_tracks_str;
gchar **deactivated_tracks = NULL;
if (!g_markup_collect_attributes (element_name, attribute_names,
attribute_values, error,
G_MARKUP_COLLECT_STRING, "priority", &strprio,
COLLECT_STR_OPT, "extractable-type-name", &extractable_type_name,
COLLECT_STR_OPT, "properties", &properties,
COLLECT_STR_OPT, "deactivated-tracks", &deactivated_tracks_str,
COLLECT_STR_OPT, "metadatas", &metadatas, G_MARKUP_COLLECT_INVALID))
return;
@ -511,8 +514,13 @@ _parse_layer (GMarkupParseContext * context, const gchar * element_name,
if (errno)
goto convertion_failed;
if (deactivated_tracks_str)
deactivated_tracks = g_strsplit (deactivated_tracks_str, " ", -1);
ges_base_xml_formatter_add_layer (GES_BASE_XML_FORMATTER (self),
extractable_type, priority, props, metadatas, error);
extractable_type, priority, props, metadatas, deactivated_tracks, error);
g_strfreev (deactivated_tracks);
done:
if (props)
@ -1084,7 +1092,8 @@ _init_value_from_spec_for_serialization (GValue * value, GParamSpec * spec)
}
static gchar *
_serialize_properties (GObject * object, const gchar * fieldname, ...)
_serialize_properties (GObject * object, gint * ret_n_props,
const gchar * fieldname, ...)
{
gchar *ret;
guint n_props, j;
@ -1097,22 +1106,31 @@ _serialize_properties (GObject * object, const gchar * fieldname, ...)
GValue val = { 0 };
spec = pspecs[j];
if (spec->value_type == GST_TYPE_CAPS) {
GstCaps *caps;
gchar *caps_str;
if (!_can_serialize_spec (spec))
continue;
_init_value_from_spec_for_serialization (&val, spec);
g_object_get_property (object, spec->name, &val);
if (gst_value_compare (g_param_spec_get_default_value (spec),
&val) == GST_VALUE_EQUAL) {
GST_INFO ("Ignoring %s as it is using the default value", spec->name);
goto next;
}
if (spec->value_type == GST_TYPE_CAPS) {
gchar *caps_str;
const GstCaps *caps = gst_value_get_caps (&val);
g_object_get (object, spec->name, &caps, NULL);
caps_str = gst_caps_to_string (caps);
gst_structure_set (structure, spec->name, G_TYPE_STRING, caps_str, NULL);
if (caps)
gst_caps_unref (caps);
g_free (caps_str);
} else if (_can_serialize_spec (spec)) {
_init_value_from_spec_for_serialization (&val, spec);
g_object_get_property (object, spec->name, &val);
gst_structure_set_value (structure, spec->name, &val);
g_value_unset (&val);
goto next;
}
gst_structure_set_value (structure, spec->name, &val);
next:
g_value_unset (&val);
}
g_free (pspecs);
@ -1124,6 +1142,8 @@ _serialize_properties (GObject * object, const gchar * fieldname, ...)
}
ret = gst_structure_to_string (structure);
if (ret_n_props)
*ret_n_props = gst_structure_n_fields (structure);
gst_structure_free (structure);
return ret;
@ -1190,7 +1210,7 @@ _save_subproject (GESXmlFormatter * self, GString * str, GESProject * project,
subproject = ges_extractable_get_asset (GES_EXTRACTABLE (timeline));
substr = g_string_new (NULL);
properties = _serialize_properties (G_OBJECT (subproject), NULL);
properties = _serialize_properties (G_OBJECT (subproject), NULL, NULL);
metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (subproject));
append_escaped (str,
g_markup_printf_escaped
@ -1248,7 +1268,7 @@ _serialize_streams (GESXmlFormatter * self, GString * str,
ges_uri_source_asset_get_stream_info (tmp->data);
GstCaps *caps = gst_discoverer_stream_info_get_caps (sinfo);
properties = _serialize_properties (tmp->data, NULL);
properties = _serialize_properties (tmp->data, NULL, NULL);
metas = ges_meta_container_metas_to_string (tmp->data);
capsstr = gst_caps_to_string (caps);
@ -1299,7 +1319,7 @@ _save_assets (GESXmlFormatter * self, GString * str, GESProject * project,
G_UNLOCK (uri_subprojects_map_lock);
}
properties = _serialize_properties (G_OBJECT (asset), NULL);
properties = _serialize_properties (G_OBJECT (asset), NULL, NULL);
metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (asset));
append_escaped (str,
g_markup_printf_escaped
@ -1363,7 +1383,7 @@ _save_tracks (GESXmlFormatter * self, GString * str, GESTimeline * timeline,
tracks = ges_timeline_get_tracks (timeline);
for (tmp = tracks; tmp; tmp = tmp->next) {
track = GES_TRACK (tmp->data);
properties = _serialize_properties (G_OBJECT (track), "caps", NULL);
properties = _serialize_properties (G_OBJECT (track), NULL, "caps", NULL);
strtmp = gst_caps_to_string (ges_track_get_caps (track));
metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (track));
append_escaped (str,
@ -1512,7 +1532,7 @@ _save_effect (GString * str, guint clip_id, GESTrackElement * trackelement,
}
g_list_free_full (tracks, gst_object_unref);
properties = _serialize_properties (G_OBJECT (trackelement), "start",
properties = _serialize_properties (G_OBJECT (trackelement), NULL, "start",
"in-point", "duration", "locked", "max-duration", "name", "priority",
NULL);
metas =
@ -1537,6 +1557,79 @@ _save_effect (GString * str, guint clip_id, GESTrackElement * trackelement,
depth);
}
static inline void
_save_layer_track_activness (GESXmlFormatter * self, GESLayer * layer,
GString * str, GESTimeline * timeline, guint depth)
{
guint nb_tracks = 0, i;
GList *tmp, *tracks = ges_timeline_get_tracks (timeline);
GArray *deactivated_tracks = g_array_new (TRUE, FALSE, sizeof (gint32));
for (tmp = tracks; tmp; tmp = tmp->next, nb_tracks++) {
if (!ges_layer_get_active_for_track (layer, tmp->data))
g_array_append_val (deactivated_tracks, nb_tracks);
}
if (!deactivated_tracks->len) {
g_string_append (str, ">\n");
goto done;
}
self->priv->min_version = MAX (self->priv->min_version, 7);
g_string_append (str, " deactivated-tracks='");
for (i = 0; i < deactivated_tracks->len; i++)
g_string_append_printf (str, "%d ", g_array_index (deactivated_tracks, gint,
i));
g_string_append (str, "'>\n");
done:
g_array_free (deactivated_tracks, TRUE);
g_list_free_full (tracks, gst_object_unref);
}
static void
_save_source (GESXmlFormatter * self, GString * str,
GESTimelineElement * element, GESTimeline * timeline, GList * tracks,
guint depth)
{
gint index, n_props;
gboolean serialize;
gchar *properties;
if (!GES_IS_SOURCE (element))
return;
g_object_get (element, "serialize", &serialize, NULL);
if (!serialize) {
GST_DEBUG_OBJECT (element, "Should not be serialized");
return;
}
index =
g_list_index (tracks,
ges_track_element_get_track (GES_TRACK_ELEMENT (element)));
append_escaped (str,
g_markup_printf_escaped
(" <source track-id='%i' ", index), depth);
properties = _serialize_properties (G_OBJECT (element), &n_props,
"in-point", "priority", "start", "duration", "track", "track-type"
"uri", "name", "max-duration", NULL);
/* Try as possible to allow older versions of GES to load the files */
if (n_props) {
self->priv->min_version = MAX (self->priv->min_version, 7);
g_string_append_printf (str, "properties='%s' ", properties);
}
g_free (properties);
_save_children_properties (str, element, depth);
append_escaped (str, g_markup_printf_escaped (">\n"), depth);
_save_keyframes (str, GES_TRACK_ELEMENT (element), index, depth);
append_escaped (str, g_markup_printf_escaped (" </source>\n"),
depth);
}
static inline void
_save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline,
guint depth)
@ -1552,15 +1645,18 @@ _save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline,
layer = GES_LAYER (tmplayer->data);
priority = ges_layer_get_priority (layer);
properties = _serialize_properties (G_OBJECT (layer), "priority", NULL);
properties =
_serialize_properties (G_OBJECT (layer), NULL, "priority", NULL);
metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (layer));
append_escaped (str,
g_markup_printf_escaped
(" <layer priority='%i' properties='%s' metadatas='%s'>\n",
(" <layer priority='%i' properties='%s' metadatas='%s'",
priority, properties, metas), depth);
g_free (properties);
g_free (metas);
_save_layer_track_activness (self, layer, str, timeline, depth);
clips = ges_layer_get_clips (layer);
for (tmpclip = clips; tmpclip; tmpclip = tmpclip->next) {
GList *effects, *tmpeffect;
@ -1579,7 +1675,7 @@ _save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline,
/* We escape all mandatrorry properties that are handled sparetely
* and vtype for StandarTransition as it is the asset ID */
properties = _serialize_properties (G_OBJECT (clip),
properties = _serialize_properties (G_OBJECT (clip), NULL,
"supported-formats", "rate", "in-point", "start", "duration",
"max-duration", "priority", "vtype", "uri", NULL);
extractable_id = ges_extractable_get_id (GES_EXTRACTABLE (clip));
@ -1629,31 +1725,9 @@ _save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline,
for (tmptrackelement = GES_CONTAINER_CHILDREN (clip); tmptrackelement;
tmptrackelement = tmptrackelement->next) {
gint index;
gboolean serialize;
if (!GES_IS_SOURCE (tmptrackelement->data))
continue;
g_object_get (tmptrackelement->data, "serialize", &serialize, NULL);
if (!serialize) {
GST_DEBUG_OBJECT (tmptrackelement->data, "Should not be serialized");
continue;
}
index =
g_list_index (tracks,
ges_track_element_get_track (tmptrackelement->data));
append_escaped (str,
g_markup_printf_escaped (" <source track-id='%i'", index),
depth);
_save_children_properties (str, tmptrackelement->data, depth);
append_escaped (str, g_markup_printf_escaped (">\n"), depth);
_save_keyframes (str, tmptrackelement->data, index, depth);
append_escaped (str, g_markup_printf_escaped (" </source>\n"),
_save_source (self, str, tmptrackelement->data, timeline, tracks,
depth);
}
g_list_free_full (tracks, gst_object_unref);
string_append_with_depth (str, " </clip>\n", depth);
@ -1695,7 +1769,7 @@ _save_group (GESXmlFormatter * self, GString * str, GList ** seen_groups,
}
}
properties = _serialize_properties (G_OBJECT (group), NULL);
properties = _serialize_properties (G_OBJECT (group), NULL, NULL);
metadatas = ges_meta_container_metas_to_string (GES_META_CONTAINER (group));
self->priv->min_version = MAX (self->priv->min_version, 5);
@ -1742,7 +1816,8 @@ _save_timeline (GESXmlFormatter * self, GString * str, GESTimeline * timeline,
{
gchar *properties = NULL, *metas = NULL;
properties = _serialize_properties (G_OBJECT (timeline), "update", "name",
properties =
_serialize_properties (G_OBJECT (timeline), NULL, "update", "name",
"async-handling", "message-forward", NULL);
ges_meta_container_set_uint64 (GES_META_CONTAINER (timeline), "duration",
@ -1815,10 +1890,10 @@ _save_stream_profiles (GESXmlFormatter * self, GString * str,
if (GST_IS_PRESET (encoder) &&
gst_preset_load_preset (GST_PRESET (encoder), preset)) {
gchar *settings = _serialize_properties (G_OBJECT (encoder), NULL);
append_escaped (str,
g_markup_printf_escaped ("preset-properties='%s' ", settings),
depth);
gchar *settings =
_serialize_properties (G_OBJECT (encoder), NULL, NULL);
append_escaped (str, g_markup_printf_escaped ("preset-properties='%s' ",
settings), depth);
g_free (settings);
}
gst_object_unref (encoder);
@ -1893,7 +1968,8 @@ _save_encoding_profiles (GESXmlFormatter * self, GString * str,
if (element) {
if (GST_IS_PRESET (element) &&
gst_preset_load_preset (GST_PRESET (element), profpreset)) {
gchar *settings = _serialize_properties (G_OBJECT (element), NULL);
gchar *settings =
_serialize_properties (G_OBJECT (element), NULL, NULL);
append_escaped (str,
g_markup_printf_escaped ("preset-properties='%s' ", settings),
depth);
@ -1960,7 +2036,7 @@ _save_project (GESFormatter * formatter, GString * str, GESProject * project,
GESXmlFormatter *self = GES_XML_FORMATTER (formatter);
GESXmlFormatterPrivate *priv = _GET_PRIV (formatter);
properties = _serialize_properties (G_OBJECT (project), NULL);
properties = _serialize_properties (G_OBJECT (project), NULL, NULL);
metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (project));
append_escaped (str,
g_markup_printf_escaped (" <project properties='%s' metadatas='%s'>\n",

View file

@ -77,6 +77,7 @@ if gstvalidate_dep.found()
'check_video_track_restriction_scale_with_keyframes',
'check_edit_in_frames',
'check_edit_in_frames_with_framerate_mismatch',
'check_layer_activness_gaps',
]
env = environment()

View file

@ -26,6 +26,7 @@ gi.require_version("GES", "1.0")
from gi.repository import Gst # noqa
from gi.repository import GES # noqa
from gi.repository import GLib # noqa
from gi.repository import GObject # noqa
import contextlib # noqa
import os #noqa
import unittest # noqa
@ -208,6 +209,93 @@ class GESSimpleTimelineTest(GESTest):
return clip
def assertElementAreEqual(self, ref, element):
self.assertTrue(isinstance(element, type(ref)), "%s and %s do not have the same type!" % (ref, element))
props = [p for p in ref.list_properties() if p.name not in ['name']
and not GObject.type_is_a(p.value_type, GObject.Object)]
for p in props:
pname = p.name
v0 = GObject.Value()
v0.init(p.value_type)
v0.set_value(ref.get_property(pname))
v1 = GObject.Value()
v1.init(p.value_type)
v1.set_value(element.get_property(pname))
self.assertTrue(Gst.value_compare(v0, v1) == Gst.VALUE_EQUAL,
"%s are not equal: %s != %s" % (pname, v0, v1))
if isinstance(ref, GES.TrackElement):
self.assertElementAreEqual(ref.get_nleobject(), element.get_nleobject())
return
if not isinstance(ref, GES.Clip):
return
ttypes = [track.type for track in self.timeline.get_tracks()]
for ttype in ttypes:
if ttypes.count(ttype) > 1:
self.warning("Can't deeply check %s and %s "
"(only one track per type supported %s %s found)" % (ref,
element, ttypes.count(ttype), ttype))
return
children = element.get_children(False)
for ref_child in ref.get_children(False):
ref_track = ref_child.get_track()
if not ref_track:
self.warning("Can't check %s as not in a track" % (ref_child))
continue
child = None
for tmpchild in children:
if not isinstance(tmpchild, type(ref_child)):
continue
if ref_track.type != tmpchild.get_track().type:
continue
if not isinstance(ref_child, GES.Effect):
child = tmpchild
break
elif ref_child.props.bin_description == child.props.bin_description:
child = tmpchild
break
self.assertIsNotNone(child, "Could not find equivalent child %s in %s(%s)" % (ref_child,
element, children))
self.assertElementAreEqual(ref_child, child)
def check_reload_timeline(self):
tmpf = tempfile.NamedTemporaryFile(suffix='.xges')
uri = Gst.filename_to_uri(tmpf.name)
self.assertTrue(self.timeline.save_to_uri(uri, None, True))
project = GES.Project.new(uri)
mainloop = create_main_loop()
def loaded_cb(unused_project, unused_timeline):
mainloop.quit()
project.connect("loaded", loaded_cb)
reloaded_timeline = project.extract()
mainloop.run()
self.assertIsNotNone(reloaded_timeline)
layers = self.timeline.get_layers()
reloaded_layers = reloaded_timeline.get_layers()
self.assertEqual(len(layers), len(reloaded_layers))
for layer, reloaded_layer in zip(layers, reloaded_layers):
clips = layer.get_clips()
reloaded_clips = reloaded_layer.get_clips()
self.assertEqual(len(clips), len(reloaded_clips))
for clip, reloaded_clip in zip(clips, reloaded_clips):
self.assertElementAreEqual(clip, reloaded_clip)
return reloaded_timeline
def assertTimelineTopology(self, topology, groups=[]):
res = []
for layer in self.timeline.get_layers():

View file

@ -222,6 +222,107 @@ class TestTimeline(common.GESSimpleTimelineTest):
self.assertEqual(self.timeline.get_frame_at(Gst.SECOND), 60)
self.assertEqual(clip.props.max_duration, Gst.SECOND)
def test_layer_active(self):
def check_nle_object_activeness(clip, track_type, active=None, ref_clip=None):
assert ref_clip is not None or active is not None
if ref_clip:
ref_elem, = ref_clip.find_track_elements(None, track_type, GES.Source)
active = ref_elem.get_nleobject().props.active
elem, = clip.find_track_elements(None, track_type, GES.Source)
self.assertIsNotNone(elem)
self.assertEqual(elem.get_nleobject().props.active, active)
def get_tracks(timeline):
for track in self.timeline.get_tracks():
if track.props.track_type == GES.TrackType.VIDEO:
video_track = track
else:
audio_track = track
return video_track, audio_track
def check_set_active_for_tracks(layer, active, tracks, expected_changed_tracks):
callback_called = []
def _check_active_changed_cb(layer, active, tracks, expected_tracks, expected_active):
self.assertEqual(set(tracks), set(expected_tracks))
self.assertEqual(active, expected_active)
callback_called.append(True)
layer.connect("active-changed", _check_active_changed_cb, expected_changed_tracks, active)
self.assertTrue(layer.set_active_for_tracks(active, tracks))
self.layer.disconnect_by_func(_check_active_changed_cb)
self.assertEqual(callback_called, [True])
c0 = self.append_clip()
check_nle_object_activeness(c0, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c0, GES.TrackType.AUDIO, True)
elem, = c0.find_track_elements(None, GES.TrackType.AUDIO, GES.Source)
elem.props.active = False
check_nle_object_activeness(c0, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c0, GES.TrackType.AUDIO, False)
self.check_reload_timeline()
elem.props.active = True
# Muting audio track
video_track, audio_track = get_tracks(self.timeline)
check_set_active_for_tracks(self.layer, False, [audio_track], [audio_track])
check_nle_object_activeness(c0, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c0, GES.TrackType.AUDIO, False)
self.check_reload_timeline()
c1 = self.append_clip()
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, False)
l1 = self.timeline.append_layer()
c1.move_to_layer(l1)
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, True)
self.assertTrue(c1.edit([], self.layer.get_priority(), GES.EditMode.EDIT_NORMAL,
GES.Edge.EDGE_NONE, c1.props.start))
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, False)
self.check_reload_timeline()
self.assertTrue(self.layer.remove_clip(c1))
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, True)
self.assertTrue(self.layer.add_clip(c1))
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, False)
check_set_active_for_tracks(self.layer, True, None, [audio_track])
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, True)
elem, = c1.find_track_elements(None, GES.TrackType.AUDIO, GES.Source)
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, True)
# Force deactivating a specific TrackElement
elem.props.active = False
check_nle_object_activeness(c1, GES.TrackType.VIDEO, True)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, False)
self.check_reload_timeline()
# Try activating a specific TrackElement, that won't change the
# underlying nleobject activness
check_set_active_for_tracks(self.layer, False, None, [audio_track, video_track])
check_nle_object_activeness(c1, GES.TrackType.VIDEO, False)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, False)
elem.props.active = True
check_nle_object_activeness(c1, GES.TrackType.VIDEO, False)
check_nle_object_activeness(c1, GES.TrackType.AUDIO, False)
self.check_reload_timeline()
class TestEditing(common.GESSimpleTimelineTest):

View file

@ -0,0 +1,24 @@
description, handles-states=true,
ges-options={\
"--disable-mixing",
"--videosink=fakevideosink",
"--audiosink=fakesink"\
}
add-clip, name=clip, asset-id="framerate=30/1", layer-priority=0, type=GESTestClip, pattern=blue, duration=5000.0
set-layer-active, tracks={gesvideotrack0}, active=false, layer-priority=0
pause;
# Make sure the video test src is a gap test src.
check-property, target-element-factory-name=videotestsrc, property-name=pattern, property-value="100% Black"
check-property, target-element-factory-name=audiotestsrc, property-name=wave, property-value="Sine"
set-layer-active, tracks={gesvideotrack0}, active=true, layer-priority=0
set-layer-active, tracks={gesaudiotrack0}, active=false, layer-priority=0
commit;
# Make sure the video test src is the GESVideoTestSource and the audio test source is a gap
check-property, target-element-factory-name=videotestsrc, property-name=pattern, property-value="Blue"
check-property, target-element-factory-name=audiotestsrc, property-name=wave, property-value="Silence"
stop;