diff --git a/ges/ges-base-xml-formatter.c b/ges/ges-base-xml-formatter.c index 77877cea9c..7e5e1268d8 100644 --- a/ges/ges-base-xml-formatter.c +++ b/ges/ges-base-xml-formatter.c @@ -73,7 +73,6 @@ typedef enum struct _GESBaseXmlFormatterPrivate { GMarkupParseContext *parsecontext; - gchar *xmlcontent; gsize xmlsize; LoadingState state; @@ -139,6 +138,12 @@ G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GESBaseXmlFormatter, static gint compare_assets_for_loading (PendingAsset * a, PendingAsset * b) { + if (a->extractable_type == GES_TYPE_TIMELINE) + return -1; + + if (b->extractable_type == GES_TYPE_TIMELINE) + return 1; + if (a->proxy_id) return -1; @@ -157,7 +162,7 @@ _parse (GESBaseXmlFormatter * self, GError ** error, LoadingState state) GES_BASE_XML_FORMATTER_GET_CLASS (self); GESBaseXmlFormatterPrivate *priv = _GET_PRIV (self); - if (!priv->xmlcontent || g_strcmp0 (priv->xmlcontent, "") == 0) { + if (!self->xmlcontent || g_strcmp0 (self->xmlcontent, "") == 0) { err = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, "Nothing contained in the project file."); @@ -169,7 +174,7 @@ _parse (GESBaseXmlFormatter * self, GError ** error, LoadingState state) priv->state = state; GST_DEBUG_OBJECT (self, "Running %s pass", loading_state_name (state)); - if (!g_markup_parse_context_parse (parsecontext, priv->xmlcontent, + if (!g_markup_parse_context_parse (parsecontext, self->xmlcontent, priv->xmlsize, &err)) goto failed; @@ -215,7 +220,8 @@ _load_and_parse (GESBaseXmlFormatter * self, const gchar * uri, GError ** error, GError *err = NULL; - GST_INFO_OBJECT (self, "loading xml from %s", uri); + GST_DEBUG_OBJECT (self, "loading xml from %s, %s", uri, + loading_state_name (state)); file = g_file_new_for_uri (uri); /* TODO Handle GCancellable */ @@ -225,8 +231,8 @@ _load_and_parse (GESBaseXmlFormatter * self, const gchar * uri, GError ** error, goto failed; } - g_clear_pointer (&priv->xmlcontent, g_free); - if (!g_file_load_contents (file, NULL, &priv->xmlcontent, &priv->xmlsize, + g_clear_pointer (&self->xmlcontent, g_free); + if (!g_file_load_contents (file, NULL, &self->xmlcontent, &priv->xmlsize, NULL, &err)) goto failed; g_object_unref (file); @@ -374,11 +380,12 @@ _dispose (GObject * object) static void _finalize (GObject * object) { + GESBaseXmlFormatter *self = GES_BASE_XML_FORMATTER (object); GESBaseXmlFormatterPrivate *priv = _GET_PRIV (object); if (priv->parsecontext != NULL) g_markup_parse_context_free (priv->parsecontext); - g_free (priv->xmlcontent); + g_clear_pointer (&self->xmlcontent, g_free); g_list_free_full (priv->groups, (GDestroyNotify) _free_pending_group); priv->groups = NULL; diff --git a/ges/ges-base-xml-formatter.h b/ges/ges-base-xml-formatter.h index fafde1aa86..b348521d02 100644 --- a/ges/ges-base-xml-formatter.h +++ b/ges/ges-base-xml-formatter.h @@ -45,8 +45,9 @@ struct _GESBaseXmlFormatter /*< public > */ /* */ GESBaseXmlFormatterPrivate *priv; + gchar *xmlcontent; - gpointer _ges_reserved[GES_PADDING]; + gpointer _ges_reserved[GES_PADDING - 1]; }; /** diff --git a/ges/ges-internal.h b/ges/ges-internal.h index e0c4eda9d2..a0c3e65e59 100644 --- a/ges/ges-internal.h +++ b/ges/ges-internal.h @@ -326,6 +326,12 @@ G_GNUC_INTERNAL void ges_base_xml_formatter_add_control_binding (GESBaseXmlForma const gchar *track_id, GSList * timed_values); +G_GNUC_INTERNAL void ges_base_xml_formatter_set_timeline_properties(GESBaseXmlFormatter * self, + GESTimeline *timeline, + const gchar *properties, + const gchar *metadatas); +G_GNUC_INTERNAL void ges_xml_formatter_deinit (void); + G_GNUC_INTERNAL gboolean set_property_foreach (GQuark field_id, const GValue * value, GObject * object); @@ -345,11 +351,6 @@ G_GNUC_INTERNAL gint element_end_compare (GESTimelineElement * G_GNUC_INTERNAL GstElementFactory * ges_get_compositor_factory (void); -G_GNUC_INTERNAL void -ges_base_xml_formatter_set_timeline_properties(GESBaseXmlFormatter * self, - GESTimeline *timeline, - const gchar *properties, - const gchar *metadatas); /**************************************************** * GESContainer * diff --git a/ges/ges-project.c b/ges/ges-project.c index 1311a7c179..ba9c56802a 100644 --- a/ges/ges-project.c +++ b/ges/ges-project.c @@ -43,6 +43,24 @@ * It lets you request new asset, and it informs you about new assets through * a set of signals. Also it handles problem such as missing files/missing * #GstElement and lets you try to recover from those. + * + * ## Subprojects + * + * In order to add a subproject, the only thing to do is to add the subproject + * with to the main project: + * + * ``` c + * ges_project_add_asset (project, GES_ASSET (subproject)); + * ``` + * then the subproject will be serialized in the project files. To use + * the subproject in a timeline, you should use a #GESUriClip with the + * same subproject URI. + * + * When loading a project with subproject, subprojects URIs will be temporary + * writable local files. If you want to edit the subproject timeline, + * you should retrieve the subproject from the parent project asset list and + * extract the timeline with ges_asset_extract() and save it at + * the same temporary location. */ #ifdef HAVE_CONFIG_H #include "config.h" diff --git a/ges/ges-xml-formatter.c b/ges/ges-xml-formatter.c index 8468eb40d1..c3fbf2bd21 100644 --- a/ges/ges-xml-formatter.c +++ b/ges/ges-xml-formatter.c @@ -30,6 +30,7 @@ #include #include "ges.h" +#include #include "ges-internal.h" #define parent_class ges_xml_formatter_parent_class @@ -41,6 +42,17 @@ #define _GET_PRIV(o) (((GESXmlFormatter*)o)->priv) +typedef struct +{ + const gchar *id; + gint start_line; + gint start_char; + gint fd; + gchar *filename; + GError *error; + GMainLoop *ml; +} SubprojectData; + struct _GESXmlFormatterPrivate { gboolean ges_opened; @@ -49,15 +61,25 @@ struct _GESXmlFormatterPrivate GString *str; GHashTable *element_id; + GHashTable *subprojects_map; + SubprojectData *subproject; + gint subproject_depth; guint nbelements; guint min_version; }; +G_LOCK_DEFINE_STATIC (uri_subprojects_map_lock); +/* { project_uri: { subproject_uri: new_suproject_uri}} */ +static GHashTable *uri_subprojects_map = NULL; + G_DEFINE_TYPE_WITH_PRIVATE (GESXmlFormatter, ges_xml_formatter, GES_TYPE_BASE_XML_FORMATTER); +static GString *_save_project (GESFormatter * formatter, GString * str, + GESProject * project, GESTimeline * timeline, GError ** error, guint depth); + static inline void _parse_ges_element (GMarkupParseContext * context, const gchar * element_name, const gchar ** attribute_names, const gchar ** attribute_values, @@ -71,7 +93,7 @@ _parse_ges_element (GMarkupParseContext * context, const gchar * element_name, if (g_strcmp0 (element_name, "ges")) { g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, - "element '%s', Missing element'", element_name); + "Found element '%s', Missing '' element'", element_name); return; } @@ -301,6 +323,7 @@ _parse_asset (GMarkupParseContext * context, const gchar * element_name, GType extractable_type; const gchar *id, *extractable_type_name, *metadatas = NULL, *properties = NULL, *proxy_id = NULL; + GESXmlFormatterPrivate *priv = _GET_PRIV (self); if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error, G_MARKUP_COLLECT_STRING, "id", &id, @@ -312,6 +335,39 @@ _parse_asset (GMarkupParseContext * context, const gchar * element_name, return; extractable_type = g_type_from_name (extractable_type_name); + if (extractable_type == GES_TYPE_TIMELINE) { + SubprojectData *subproj_data = g_malloc0 (sizeof (SubprojectData)); + const gchar *nid; + + priv->subproject = subproj_data; + G_LOCK (uri_subprojects_map_lock); + nid = g_hash_table_lookup (priv->subprojects_map, id); + G_UNLOCK (uri_subprojects_map_lock); + + if (!nid) { + subproj_data->id = id; + subproj_data->fd = + g_file_open_tmp ("XXXXXX.xges", &subproj_data->filename, error); + if (subproj_data->fd == -1) { + GST_ERROR_OBJECT (self, "Could not create subproject file for %s", id); + return; + } + g_markup_parse_context_get_position (context, &subproj_data->start_line, + &subproj_data->start_char); + id = gst_uri_construct ("file", subproj_data->filename); + G_LOCK (uri_subprojects_map_lock); + g_hash_table_insert (priv->subprojects_map, g_strdup (subproj_data->id), + (gchar *) id); + G_UNLOCK (uri_subprojects_map_lock); + GST_INFO_OBJECT (self, "Serialized subproject %sis now at: %s", + subproj_data->id, id); + } else { + GST_DEBUG_OBJECT (self, "Subproject already exists: %s -> %s", id, nid); + id = nid; + subproj_data->start_line = -1; + } + } + if (extractable_type == G_TYPE_NONE) g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, @@ -327,6 +383,16 @@ _parse_asset (GMarkupParseContext * context, const gchar * element_name, if (properties) props = gst_structure_from_string (properties, NULL); + if (extractable_type == GES_TYPE_URI_CLIP) { + G_LOCK (uri_subprojects_map_lock); + if (g_hash_table_contains (priv->subprojects_map, id)) { + id = g_hash_table_lookup (priv->subprojects_map, id); + + GST_DEBUG_OBJECT (self, "Using subproject %s", id); + } + G_UNLOCK (uri_subprojects_map_lock); + } + ges_base_xml_formatter_add_asset (GES_BASE_XML_FORMATTER (self), id, extractable_type, props, metadatas, proxy_id, error); if (props) @@ -474,6 +540,7 @@ _parse_clip (GMarkupParseContext * context, GstStructure *props = NULL, *children_props = NULL; GESTrackType track_types; GstClockTime start, inpoint = 0, duration, layer_prio; + GESXmlFormatterPrivate *priv = _GET_PRIV (self); const gchar *strid, *asset_id, *strstart, *strin, *strduration, *strrate, *strtrack_types, *strtype, *metadatas = NULL, *properties = @@ -534,6 +601,12 @@ _parse_clip (GMarkupParseContext * context, goto wrong_children_properties; } + G_LOCK (uri_subprojects_map_lock); + if (g_hash_table_contains (priv->subprojects_map, asset_id)) { + asset_id = g_hash_table_lookup (priv->subprojects_map, asset_id); + GST_DEBUG_OBJECT (self, "Using subproject %s", asset_id); + } + G_UNLOCK (uri_subprojects_map_lock); ges_base_xml_formatter_add_clip (GES_BASE_XML_FORMATTER (self), strid, asset_id, type, start, inpoint, duration, layer_prio, track_types, props, children_props, metadatas, error); @@ -776,13 +849,21 @@ _parse_element_start (GMarkupParseContext * context, const gchar * element_name, { GESXmlFormatterPrivate *priv = _GET_PRIV (self); - if (!G_UNLIKELY (priv->ges_opened)) + if (priv->subproject) { + if (g_strcmp0 (element_name, "ges") == 0) { + priv->subproject_depth += 1; + } + return; + } + + if (!G_UNLIKELY (priv->ges_opened)) { _parse_ges_element (context, element_name, attribute_names, attribute_values, self, error); - else if (!G_UNLIKELY (priv->project_opened)) + } else if (!G_UNLIKELY (priv->project_opened)) _parse_project (context, element_name, attribute_names, attribute_values, self, error); - else if (g_strcmp0 (element_name, "encoding-profile") == 0) + else if (g_strcmp0 (element_name, "ges") == 0) { + } else if (g_strcmp0 (element_name, "encoding-profile") == 0) _parse_encoding_profile (context, element_name, attribute_names, attribute_values, self, error); else if (g_strcmp0 (element_name, "stream-profile") == 0) @@ -822,19 +903,91 @@ _parse_element_start (GMarkupParseContext * context, const gchar * element_name, GST_LOG_OBJECT (self, "Element %s not handled", element_name); } +static gboolean +_save_subproject_data (GESXmlFormatter * self, SubprojectData * subproj_data, + gint subproject_end_line, gint subproject_end_char, GError ** error) +{ + gsize size; + gint line = 1, i; + gboolean res = FALSE; + gsize start = 0, end = 0; + gchar *subproject_content = NULL; + gchar *xml = GES_BASE_XML_FORMATTER (self)->xmlcontent; + + for (i = 0; xml[i] != '\0'; i++) { + if (!start && line == subproj_data->start_line) { + i += subproj_data->start_char - 1; + start = i; + } + + if (line == subproject_end_line) { + end = i + subproject_end_char - 1; + break; + } + + if (xml[i] == '\n') + line++; + } + g_assert (start && end); + size = (end - start); + + subproject_content = g_malloc (sizeof (gchar) * size); + memcpy (subproject_content, &xml[start], end - start); + subproject_content[end - start] = '\0'; + GST_INFO_OBJECT (self, "Saving subproject %s from %d:%d(%" G_GSIZE_FORMAT + ") to %d:%d(%" G_GSIZE_FORMAT ")", + subproj_data->id, subproj_data->start_line, subproj_data->start_char, + start, subproject_end_line, subproject_end_char, end); + + res = g_file_set_contents (subproj_data->filename, subproject_content, -1, + error); + g_free (subproject_content); + + return res; +} + static void _parse_element_end (GMarkupParseContext * context, const gchar * element_name, gpointer self, GError ** error) { + GESXmlFormatterPrivate *priv = _GET_PRIV (self); + SubprojectData *subproj_data = priv->subproject; + /*GESXmlFormatterPrivate *priv = _GET_PRIV (self); */ - if (g_strcmp0 (element_name, "ges") == 0 && GES_FORMATTER (self)->project) { - gchar *version = g_strdup_printf ("%d.%d", - API_VERSION, GES_XML_FORMATTER (self)->priv->min_version); - ges_meta_container_set_string (GES_META_CONTAINER (GES_FORMATTER - (self)->project), GES_META_FORMAT_VERSION, version); + if (!g_strcmp0 (element_name, "ges")) { + gint subproject_end_line, subproject_end_char; - g_free (version); + if (priv->subproject_depth) + priv->subproject_depth -= 1; + + if (!subproj_data) { + if (GES_FORMATTER (self)->project) { + gchar *version = g_strdup_printf ("%d.%d", + API_VERSION, GES_XML_FORMATTER (self)->priv->min_version); + + ges_meta_container_set_string (GES_META_CONTAINER (GES_FORMATTER + (self)->project), GES_META_FORMAT_VERSION, version); + + g_free (version); + _GET_PRIV (self)->ges_opened = FALSE; + } + } else if (subproj_data->start_line != -1 && !priv->subproject_depth) { + g_markup_parse_context_get_position (context, &subproject_end_line, + &subproject_end_char); + _save_subproject_data (GES_XML_FORMATTER (self), subproj_data, + subproject_end_line, subproject_end_char, error); + + subproj_data->filename = NULL; + g_close (subproj_data->fd, error); + subproj_data->id = NULL; + subproj_data->start_line = 0; + subproj_data->start_char = 0; + } + + if (!priv->subproject_depth) { + g_clear_pointer (&priv->subproject, g_free); + } } } @@ -853,9 +1006,24 @@ _error_parsing (GMarkupParseContext * context, GError * error, /* XML writting utils */ static inline void -append_escaped (GString * str, gchar * tmpstr) +string_add_indents (GString * str, guint depth, gboolean prepend) { - g_string_append (str, tmpstr); + gint i; + for (i = 0; i < depth; i++) + prepend ? g_string_prepend (str, " ") : g_string_append (str, " "); +} + +static inline void +string_append_with_depth (GString * str, const gchar * string, guint depth) +{ + string_add_indents (str, depth, FALSE); + g_string_append (str, string); +} + +static inline void +append_escaped (GString * str, gchar * tmpstr, guint depth) +{ + string_append_with_depth (str, tmpstr, depth); g_free (tmpstr); } @@ -954,30 +1122,168 @@ _serialize_properties (GObject * object, const gchar * fieldname, ...) return ret; } -static inline void -_save_assets (GESXmlFormatter * self, GString * str, GESProject * project) +static void +project_loaded_cb (GESProject * project, GESTimeline * timeline, + SubprojectData * data) { - char *properties, *metas; + g_main_loop_quit (data->ml); +} + +static void +error_loading_asset_cb (GESProject * project, GError * err, + const gchar * unused_id, GType extractable_type, SubprojectData * data) +{ + data->error = g_error_copy (err); + g_main_loop_quit (data->ml); +} + +static gboolean +_save_subproject (GESXmlFormatter * self, GString * str, GESProject * project, + GESAsset * subproject, GError ** error, guint depth) +{ + GString *substr; + GESTimeline *timeline; + gchar *properties, *metas; + GESXmlFormatterPrivate *priv = self->priv; + GMainContext *context = g_main_context_get_thread_default (); + const gchar *id = ges_asset_get_id (subproject); + SubprojectData data = { 0, }; + + if (!g_strcmp0 (ges_asset_get_id (GES_ASSET (project)), id)) { + g_set_error (error, G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + "Project %s trying to recurse into itself", id); + return FALSE; + } + + G_LOCK (uri_subprojects_map_lock); + g_hash_table_insert (priv->subprojects_map, g_strdup (id), g_strdup (id)); + G_UNLOCK (uri_subprojects_map_lock); + timeline = GES_TIMELINE (ges_asset_extract (subproject, error)); + if (!timeline) { + return FALSE; + } + + if (!context) + context = g_main_context_default (); + + data.ml = g_main_loop_new (context, TRUE); + g_signal_connect (subproject, "loaded", (GCallback) project_loaded_cb, &data); + g_signal_connect (subproject, "error-loading-asset", + (GCallback) project_loaded_cb, &data); + g_main_loop_run (data.ml); + + g_signal_handlers_disconnect_by_func (subproject, project_loaded_cb, &data); + g_signal_handlers_disconnect_by_func (subproject, error_loading_asset_cb, + &data); + if (data.error) { + g_propagate_error (error, data.error); + return FALSE; + } + + subproject = ges_extractable_get_asset (GES_EXTRACTABLE (timeline)); + substr = g_string_new (NULL); + properties = _serialize_properties (G_OBJECT (subproject), NULL); + metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (subproject)); + append_escaped (str, + g_markup_printf_escaped + (" \n", + ges_asset_get_id (subproject), + g_type_name (ges_asset_get_extractable_type (subproject)), properties, + metas), depth); + + depth += 4; + GST_DEBUG_OBJECT (self, "Saving subproject %s (depth: %d)", + ges_asset_get_id (subproject), depth / 4); + if (!_save_project (GES_FORMATTER (self), substr, GES_PROJECT (subproject), + timeline, error, depth)) { + g_string_free (substr, TRUE); + g_object_unref (subproject); + goto err; + } + GST_DEBUG_OBJECT (self, "DONE Saving subproject %s", + ges_asset_get_id (subproject)); + depth -= 4; + + g_string_append (str, substr->str); + g_string_free (substr, TRUE); + string_append_with_depth (str, " \n", depth); + +err: + g_object_unref (subproject); + + return TRUE; +} + +static gint +sort_assets (GESAsset * a, GESAsset * b) +{ + if (GES_IS_PROJECT (a)) + return -1; + + if (GES_IS_PROJECT (b)) + return 1; + + return 0; +} + +static inline gboolean +_save_assets (GESXmlFormatter * self, GString * str, GESProject * project, + GError ** error, guint depth) +{ + gchar *properties, *metas; GESAsset *asset, *proxy; GList *assets, *tmp; + const gchar *id; + GESXmlFormatterPrivate *priv = self->priv; assets = ges_project_list_assets (project, GES_TYPE_EXTRACTABLE); - for (tmp = assets; tmp; tmp = tmp->next) { + for (tmp = g_list_sort (assets, (GCompareFunc) sort_assets); tmp; + tmp = tmp->next) { asset = GES_ASSET (tmp->data); + id = ges_asset_get_id (asset); + + if (GES_IS_PROJECT (asset)) { + if (!_save_subproject (self, str, project, asset, error, depth)) + return FALSE; + + continue; + } + + if (ges_asset_get_extractable_type (asset) == GES_TYPE_URI_CLIP) { + G_LOCK (uri_subprojects_map_lock); + if (g_hash_table_contains (priv->subprojects_map, id)) { + id = g_hash_table_lookup (priv->subprojects_map, id); + + GST_DEBUG_OBJECT (self, "Using subproject %s", id); + } + G_UNLOCK (uri_subprojects_map_lock); + } + properties = _serialize_properties (G_OBJECT (asset), NULL); metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (asset)); append_escaped (str, g_markup_printf_escaped (" subprojects_map, proxy_id)) { + proxy_id = g_hash_table_lookup (priv->subprojects_map, proxy_id); + + GST_DEBUG_OBJECT (self, "Using subproject %s", id); + } + G_UNLOCK (uri_subprojects_map_lock); + } append_escaped (str, g_markup_printf_escaped (" proxy-id='%s' ", - ges_asset_get_id (proxy))); + proxy_id), depth); if (!g_list_find (assets, proxy)) { assets = g_list_append (assets, gst_object_ref (proxy)); @@ -988,15 +1294,20 @@ _save_assets (GESXmlFormatter * self, GString * str, GESProject * project) self->priv->min_version = MAX (self->priv->min_version, 3); } + g_string_append (str, "/>\n"); g_free (properties); g_free (metas); } + g_list_free_full (assets, gst_object_unref); + + return TRUE; } static inline void -_save_tracks (GESXmlFormatter * self, GString * str, GESTimeline * timeline) +_save_tracks (GESXmlFormatter * self, GString * str, GESTimeline * timeline, + guint depth) { gchar *strtmp, *metas; GESTrack *track; @@ -1014,7 +1325,7 @@ _save_tracks (GESXmlFormatter * self, GString * str, GESTimeline * timeline) append_escaped (str, g_markup_printf_escaped (" \n", - strtmp, track->type, nb_tracks++, properties, metas)); + strtmp, track->type, nb_tracks++, properties, metas), depth); g_free (strtmp); g_free (metas); g_free (properties); @@ -1023,7 +1334,8 @@ _save_tracks (GESXmlFormatter * self, GString * str, GESTimeline * timeline) } static inline void -_save_children_properties (GString * str, GESTimelineElement * element) +_save_children_properties (GString * str, GESTimelineElement * element, + guint depth) { GstStructure *structure; GParamSpec **pspecs, *spec; @@ -1055,14 +1367,15 @@ _save_children_properties (GString * str, GESTimelineElement * element) struct_str = gst_structure_to_string (structure); append_escaped (str, - g_markup_printf_escaped (" children-properties='%s'", struct_str)); + g_markup_printf_escaped (" children-properties='%s'", struct_str), 0); gst_structure_free (structure); g_free (struct_str); } /* TODO : Use this function for every track element with controllable properties */ static inline void -_save_keyframes (GString * str, GESTrackElement * trackelement, gint index) +_save_keyframes (GString * str, GESTrackElement * trackelement, gint index, + guint depth) { GHashTable *bindings_hashtable; GHashTableIter iter; @@ -1092,12 +1405,14 @@ _save_keyframes (GString * str, GESTrackElement * trackelement, gint index) append_escaped (str, g_markup_printf_escaped (" \n")); + append_escaped (str, g_markup_printf_escaped ("'/>\n"), depth); } else GST_DEBUG ("control source not in [interpolation]"); } else @@ -1120,7 +1435,7 @@ _save_keyframes (GString * str, GESTrackElement * trackelement, gint index) static inline void _save_effect (GString * str, guint clip_id, GESTrackElement * trackelement, - GESTimeline * timeline) + GESTimeline * timeline, guint depth) { GESTrack *tck; GList *tmp, *tracks; @@ -1163,21 +1478,23 @@ _save_effect (GString * str, guint clip_id, GESTrackElement * trackelement, " type-name='%s' track-type='%i' track-id='%i' properties='%s' metadatas='%s'", extractable_id, clip_id, g_type_name (G_OBJECT_TYPE (trackelement)), tck->type, track_id, - properties, metas)); + properties, metas), depth); g_free (extractable_id); g_free (properties); g_free (metas); - _save_children_properties (str, GES_TIMELINE_ELEMENT (trackelement)); - append_escaped (str, g_markup_printf_escaped (">\n")); + _save_children_properties (str, GES_TIMELINE_ELEMENT (trackelement), depth); + append_escaped (str, g_markup_printf_escaped (">\n"), depth); - _save_keyframes (str, trackelement, -1); + _save_keyframes (str, trackelement, -1, depth); - append_escaped (str, g_markup_printf_escaped (" \n")); + append_escaped (str, g_markup_printf_escaped (" \n"), + depth); } static inline void -_save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline) +_save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline, + guint depth) { gchar *properties, *metas; GESLayer *layer; @@ -1195,7 +1512,7 @@ _save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline) append_escaped (str, g_markup_printf_escaped (" \n", - priority, properties, metas)); + priority, properties, metas), depth); g_free (properties); g_free (metas); @@ -1221,6 +1538,14 @@ _save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline) "supported-formats", "rate", "in-point", "start", "duration", "max-duration", "priority", "vtype", "uri", NULL); extractable_id = ges_extractable_get_id (GES_EXTRACTABLE (clip)); + if (GES_IS_URI_CLIP (clip)) { + G_LOCK (uri_subprojects_map_lock); + if (g_hash_table_contains (priv->subprojects_map, extractable_id)) + extractable_id = + g_strdup (g_hash_table_lookup (priv->subprojects_map, + extractable_id)); + G_UNLOCK (uri_subprojects_map_lock); + } metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (clip)); append_escaped (str, g_markup_printf_escaped (" nbelements, extractable_id, g_type_name (G_OBJECT_TYPE (clip)), priority, ges_clip_get_supported_formats (clip), _START (clip), - _DURATION (clip), _INPOINT (clip), 0, properties, metas)); + _DURATION (clip), _INPOINT (clip), 0, properties, metas), depth); g_free (metas); if (GES_IS_TRANSITION_CLIP (clip)) { - _save_children_properties (str, GES_TIMELINE_ELEMENT (clip)); + _save_children_properties (str, GES_TIMELINE_ELEMENT (clip), depth); self->priv->min_version = MAX (self->priv->min_version, 4); } g_string_append (str, ">\n"); @@ -1252,7 +1577,7 @@ _save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline) effects = ges_clip_get_top_effects (clip); for (tmpeffect = effects; tmpeffect; tmpeffect = tmpeffect->next) { _save_effect (str, priv->nbelements, - GES_TRACK_ELEMENT (tmpeffect->data), timeline); + GES_TRACK_ELEMENT (tmpeffect->data), timeline, depth); } g_list_free (effects); tracks = ges_timeline_get_tracks (timeline); @@ -1275,27 +1600,29 @@ _save_layers (GESXmlFormatter * self, GString * str, GESTimeline * timeline) g_list_index (tracks, ges_track_element_get_track (tmptrackelement->data)); append_escaped (str, - g_markup_printf_escaped (" data); - append_escaped (str, g_markup_printf_escaped (">\n")); - _save_keyframes (str, tmptrackelement->data, index); - append_escaped (str, g_markup_printf_escaped (" \n")); + g_markup_printf_escaped (" 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 (" \n"), + depth); } g_list_free_full (tracks, gst_object_unref); - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); priv->nbelements++; } g_list_free_full (clips, (GDestroyNotify) gst_object_unref); - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); } } static void _save_group (GESXmlFormatter * self, GString * str, GList ** seen_groups, - GESGroup * group) + GESGroup * group, guint depth) { GList *tmp; gboolean serialize; @@ -1319,7 +1646,7 @@ _save_group (GESXmlFormatter * self, GString * str, GList ** seen_groups, for (tmp = GES_CONTAINER_CHILDREN (group); tmp; tmp = tmp->next) { if (GES_IS_GROUP (tmp->data)) { _save_group (self, str, seen_groups, - GES_GROUP (GES_TIMELINE_ELEMENT (tmp->data))); + GES_GROUP (GES_TIMELINE_ELEMENT (tmp->data)), depth); } } @@ -1328,6 +1655,7 @@ _save_group (GESXmlFormatter * self, GString * str, GList ** seen_groups, metadatas = ges_meta_container_metas_to_string (GES_META_CONTAINER (group)); self->priv->min_version = MAX (self->priv->min_version, 5); + string_add_indents (str, depth, FALSE); g_string_append_printf (str, " \n", self->priv->nbelements, properties, metadatas); @@ -1341,28 +1669,31 @@ _save_group (GESXmlFormatter * self, GString * str, GList ** seen_groups, gint id = GPOINTER_TO_INT (g_hash_table_lookup (self->priv->element_id, tmp->data)); + string_add_indents (str, depth, FALSE); g_string_append_printf (str, " \n", id, GES_TIMELINE_ELEMENT_NAME (tmp->data)); } - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); } static void -_save_groups (GESXmlFormatter * self, GString * str, GESTimeline * timeline) +_save_groups (GESXmlFormatter * self, GString * str, GESTimeline * timeline, + guint depth) { GList *tmp; GList *seen_groups = NULL; - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); for (tmp = ges_timeline_get_groups (timeline); tmp; tmp = tmp->next) { - _save_group (self, str, &seen_groups, tmp->data); + _save_group (self, str, &seen_groups, tmp->data, depth); } g_list_free (seen_groups); - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); } static inline void -_save_timeline (GESXmlFormatter * self, GString * str, GESTimeline * timeline) +_save_timeline (GESXmlFormatter * self, GString * str, GESTimeline * timeline, + guint depth) { gchar *properties = NULL, *metas = NULL; @@ -1374,13 +1705,14 @@ _save_timeline (GESXmlFormatter * self, GString * str, GESTimeline * timeline) metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (timeline)); append_escaped (str, g_markup_printf_escaped - (" \n", properties, metas)); + (" \n", properties, metas), + depth); - _save_tracks (self, str, timeline); - _save_layers (self, str, timeline); - _save_groups (self, str, timeline); + _save_tracks (self, str, timeline, depth); + _save_layers (self, str, timeline, depth); + _save_groups (self, str, timeline, depth); - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); g_free (properties); g_free (metas); @@ -1388,7 +1720,8 @@ _save_timeline (GESXmlFormatter * self, GString * str, GESTimeline * timeline) static void _save_stream_profiles (GESXmlFormatter * self, GString * str, - GstEncodingProfile * sprof, const gchar * profilename, guint id) + GstEncodingProfile * sprof, const gchar * profilename, guint id, + guint depth) { gchar *tmpc; GstCaps *tmpcaps; @@ -1399,10 +1732,10 @@ _save_stream_profiles (GESXmlFormatter * self, GString * str, (" priv->min_version = MAX (self->priv->min_version, 2); } @@ -1410,25 +1743,26 @@ _save_stream_profiles (GESXmlFormatter * self, GString * str, tmpcaps = gst_encoding_profile_get_format (sprof); if (tmpcaps) { tmpc = gst_caps_to_string (tmpcaps); - append_escaped (str, g_markup_printf_escaped ("format='%s' ", tmpc)); + append_escaped (str, g_markup_printf_escaped ("format='%s' ", tmpc), depth); gst_caps_unref (tmpcaps); g_free (tmpc); } name = gst_encoding_profile_get_name (sprof); if (name) - append_escaped (str, g_markup_printf_escaped ("name='%s' ", name)); + append_escaped (str, g_markup_printf_escaped ("name='%s' ", name), depth); description = gst_encoding_profile_get_description (sprof); if (description) append_escaped (str, g_markup_printf_escaped ("description='%s' ", - description)); + description), depth); preset = gst_encoding_profile_get_preset (sprof); if (preset) { GstElement *encoder; - append_escaped (str, g_markup_printf_escaped ("preset='%s' ", preset)); + append_escaped (str, g_markup_printf_escaped ("preset='%s' ", preset), + depth); encoder = get_element_for_encoding_profile (sprof, GST_ELEMENT_FACTORY_TYPE_ENCODER); @@ -1438,7 +1772,8 @@ _save_stream_profiles (GESXmlFormatter * self, GString * str, gchar *settings = _serialize_properties (G_OBJECT (encoder), NULL); append_escaped (str, - g_markup_printf_escaped ("preset-properties='%s' ", settings)); + g_markup_printf_escaped ("preset-properties='%s' ", settings), + depth); g_free (settings); } gst_object_unref (encoder); @@ -1448,12 +1783,13 @@ _save_stream_profiles (GESXmlFormatter * self, GString * str, preset_name = gst_encoding_profile_get_preset_name (sprof); if (preset_name) append_escaped (str, g_markup_printf_escaped ("preset-name='%s' ", - preset_name)); + preset_name), depth); tmpcaps = gst_encoding_profile_get_restriction (sprof); if (tmpcaps) { tmpc = gst_caps_to_string (tmpcaps); - append_escaped (str, g_markup_printf_escaped ("restriction='%s' ", tmpc)); + append_escaped (str, g_markup_printf_escaped ("restriction='%s' ", tmpc), + depth); gst_caps_unref (tmpcaps); g_free (tmpc); } @@ -1464,7 +1800,7 @@ _save_stream_profiles (GESXmlFormatter * self, GString * str, append_escaped (str, g_markup_printf_escaped ("pass='%d' variableframerate='%i' ", gst_encoding_video_profile_get_pass (vp), - gst_encoding_video_profile_get_variableframerate (vp))); + gst_encoding_video_profile_get_variableframerate (vp)), depth); } g_string_append (str, "/>\n"); @@ -1472,7 +1808,7 @@ _save_stream_profiles (GESXmlFormatter * self, GString * str, static inline void _save_encoding_profiles (GESXmlFormatter * self, GString * str, - GESProject * project) + GESProject * project, guint depth) { GstCaps *profformat; const gchar *profname, *profdesc, *profpreset, *proftype, *profpresetname; @@ -1493,13 +1829,13 @@ _save_encoding_profiles (GESXmlFormatter * self, GString * str, append_escaped (str, g_markup_printf_escaped (" next, i++) { GstEncodingProfile *sprof = (GstEncodingProfile *) tmp2->data; - _save_stream_profiles (self, str, sprof, profname, i); + _save_stream_profiles (self, str, sprof, profname, i, depth); } } append_escaped (str, - g_markup_printf_escaped (" \n")); + g_markup_printf_escaped (" \n"), depth); } g_list_free (profiles); } @@ -1559,41 +1897,51 @@ _save (GESFormatter * formatter, GESTimeline * timeline, GError ** error) { GString *str; GESProject *project; - - gchar *projstr = NULL, *version; - gchar *properties = NULL, *metas = NULL; - GESXmlFormatter *self = GES_XML_FORMATTER (formatter); - GESXmlFormatterPrivate *priv; - - - priv = _GET_PRIV (formatter); + GESXmlFormatterPrivate *priv = _GET_PRIV (formatter); priv->min_version = 1; project = formatter->project; str = priv->str = g_string_new (NULL); + return _save_project (formatter, str, project, timeline, error, 0); +} + +static GString * +_save_project (GESFormatter * formatter, GString * str, GESProject * project, + GESTimeline * timeline, GError ** error, guint depth) +{ + gchar *projstr = NULL, *version; + gchar *properties = NULL, *metas = NULL; + GESXmlFormatter *self = GES_XML_FORMATTER (formatter); + GESXmlFormatterPrivate *priv = _GET_PRIV (formatter); + properties = _serialize_properties (G_OBJECT (project), NULL); metas = ges_meta_container_metas_to_string (GES_META_CONTAINER (project)); append_escaped (str, g_markup_printf_escaped (" \n", - properties, metas)); + properties, metas), depth); g_free (properties); g_free (metas); - g_string_append (str, " \n"); - _save_encoding_profiles (GES_XML_FORMATTER (formatter), str, project); - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); + _save_encoding_profiles (GES_XML_FORMATTER (formatter), str, project, depth); + string_append_with_depth (str, " \n", depth); - g_string_append (str, " \n"); - _save_assets (self, str, project); - g_string_append (str, " \n"); + string_append_with_depth (str, " \n", depth); + if (!_save_assets (self, str, project, error, depth)) { + g_string_free (str, TRUE); + return NULL; + } + string_append_with_depth (str, " \n", depth); - _save_timeline (self, str, timeline); - g_string_append (str, "\n"); + _save_timeline (self, str, timeline, depth); + string_append_with_depth (str, " \n", depth); + string_append_with_depth (str, "\n", depth); projstr = g_strdup_printf ("\n", API_VERSION, priv->min_version); g_string_prepend (str, projstr); + string_add_indents (str, depth, TRUE); g_free (projstr); ges_meta_container_set_int (GES_META_CONTAINER (project), @@ -1612,6 +1960,66 @@ _save (GESFormatter * formatter, GESTimeline * timeline, GError ** error) return str; } +static void +_setup_subprojects_map (GESXmlFormatterPrivate * priv, const gchar * uri) +{ + GHashTable *subprojects_map; + + G_LOCK (uri_subprojects_map_lock); + if (!uri_subprojects_map) + uri_subprojects_map = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, + (GDestroyNotify) g_hash_table_unref); + + subprojects_map = g_hash_table_lookup (uri_subprojects_map, uri); + if (!subprojects_map) { + subprojects_map = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (uri_subprojects_map, g_strdup (uri), subprojects_map); + } + priv->subprojects_map = subprojects_map; + G_UNLOCK (uri_subprojects_map_lock); + +} + +void +ges_xml_formatter_deinit (void) +{ + GST_DEBUG ("Deinit"); + G_LOCK (uri_subprojects_map_lock); + if (uri_subprojects_map) { + g_hash_table_unref (uri_subprojects_map); + uri_subprojects_map = NULL; + } + G_UNLOCK (uri_subprojects_map_lock); +} + +static gboolean +_save_to_uri (GESFormatter * formatter, GESTimeline * timeline, + const gchar * uri, gboolean overwrite, GError ** error) +{ + _setup_subprojects_map (_GET_PRIV (formatter), uri); + return GES_FORMATTER_CLASS (parent_class)->save_to_uri (formatter, timeline, + uri, overwrite, error); +} + +static gboolean +_can_load_uri (GESFormatter * formatter, const gchar * uri, GError ** error) +{ + _setup_subprojects_map (_GET_PRIV (formatter), uri); + return GES_FORMATTER_CLASS (parent_class)->can_load_uri (formatter, uri, + error); +} + +static gboolean +_load_from_uri (GESFormatter * formatter, GESTimeline * timeline, + const gchar * uri, GError ** error) +{ + _setup_subprojects_map (_GET_PRIV (formatter), uri); + return GES_FORMATTER_CLASS (parent_class)->load_from_uri (formatter, timeline, + uri, error); +} + /*********************************************** * * * GObject virtual methods implementation * @@ -1656,9 +2064,14 @@ ges_xml_formatter_class_init (GESXmlFormatterClass * self_class) { GObjectClass *object_class = G_OBJECT_CLASS (self_class); GESBaseXmlFormatterClass *basexmlformatter_class; + GESFormatterClass *formatter_klass = GES_FORMATTER_CLASS (self_class); basexmlformatter_class = GES_BASE_XML_FORMATTER_CLASS (self_class); + formatter_klass->save_to_uri = _save_to_uri; + formatter_klass->can_load_uri = _can_load_uri; + formatter_klass->load_from_uri = _load_from_uri; + object_class->get_property = _get_property; object_class->set_property = _set_property; object_class->dispose = _dispose; diff --git a/ges/ges.c b/ges/ges.c index 8781b2ad08..eb81fadb16 100644 --- a/ges/ges.c +++ b/ges/ges.c @@ -26,13 +26,6 @@ * GES needs to be initialized after GStreamer itself. This section * contains the various functions to do so. */ -/* TODO - * Add a deinit function - * - * Do not forget to - * + g_ptr_array_unref (new_paths); - * + g_hash_table_unref (tried_uris); - */ #ifdef HAVE_CONFIG_H #include "config.h" @@ -229,6 +222,7 @@ ges_deinit (void) g_type_class_unref (g_type_class_peek (GES_TYPE_EFFECT)); ges_asset_cache_deinit (); + ges_xml_formatter_deinit (); initialized_thread = NULL; G_UNLOCK (init_lock); diff --git a/tests/check/python/common.py b/tests/check/python/common.py index 52d62c8cf0..7bf43913b1 100644 --- a/tests/check/python/common.py +++ b/tests/check/python/common.py @@ -62,24 +62,27 @@ def create_main_loop(): def create_project(with_group=False, saved=False): """Creates a project with two clips in a group.""" - project = GES.Project.new(None) - timeline = project.extract() + timeline = GES.Timeline.new_audio_video() layer = timeline.append_layer() if with_group: clip1 = GES.TitleClip() clip1.set_start(0) - clip1.set_duration(10) + clip1.set_duration(10*Gst.SECOND) layer.add_clip(clip1) clip2 = GES.TitleClip() - clip2.set_start(100) - clip2.set_duration(10) + clip2.set_start(100 * Gst.SECOND) + clip2.set_duration(10*Gst.SECOND) layer.add_clip(clip2) group = GES.Container.group([clip1, clip2]) if saved: - uri = "file://%s" % tempfile.NamedTemporaryFile(suffix=".xges").name - project.save(timeline, uri, None, overwrite=True) + if isinstance(saved, str): + suffix = "-%s.xges" % saved + else: + suffix = ".xges" + uri = "file://%s" % tempfile.NamedTemporaryFile(suffix=suffix).name + timeline.get_asset().save(timeline, uri, None, overwrite=True) return timeline diff --git a/tests/check/python/test_timeline.py b/tests/check/python/test_timeline.py index 4a02c4e2f3..2660d486a4 100644 --- a/tests/check/python/test_timeline.py +++ b/tests/check/python/test_timeline.py @@ -19,6 +19,7 @@ from . import overrides_hack +import tempfile # noqa import gi gi.require_version("Gst", "1.0") @@ -45,7 +46,6 @@ class TestTimeline(common.GESSimpleTimelineTest): project = GES.Project.new(uri=timeline.get_asset().props.uri) loaded_called = False - def loaded(unused_project, unused_timeline): nonlocal loaded_called loaded_called = True @@ -63,6 +63,77 @@ class TestTimeline(common.GESSimpleTimelineTest): self.assertTrue(loaded_called) handle.assert_not_called() + def test_deeply_nested_serialization(self): + deep_timeline = common.create_project(with_group=True, saved="deep") + deep_project = deep_timeline.get_asset() + + deep_asset = GES.UriClipAsset.request_sync(deep_project.props.id) + + nested_timeline = common.create_project(with_group=False, saved=False) + nested_project = nested_timeline.get_asset() + nested_project.add_asset(deep_project) + nested_timeline.append_layer().add_asset(deep_asset, 0, 0, 5 * Gst.SECOND, GES.TrackType.UNKNOWN) + + uri = "file://%s" % tempfile.NamedTemporaryFile(suffix="-nested.xges").name + nested_timeline.get_asset().save(nested_timeline, uri, None, overwrite=True) + + asset = GES.UriClipAsset.request_sync(nested_project.props.id) + project = self.timeline.get_asset() + project.add_asset(nested_project) + refclip = self.layer.add_asset(asset, 0, 0, 5 * Gst.SECOND, GES.TrackType.VIDEO) + + uri = "file://%s" % tempfile.NamedTemporaryFile(suffix=".xges").name + project.save(self.timeline, uri, None, overwrite=True) + self.assertEqual(len(project.list_assets(GES.Extractable)), 2) + + mainloop = common.create_main_loop() + def loaded_cb(unused_project, unused_timeline): + mainloop.quit() + project.connect("loaded", loaded_cb) + + # Extract again the timeline and compare with previous one. + timeline = project.extract() + mainloop.run() + layer, = timeline.get_layers() + clip, = layer.get_clips() + self.assertEqual(clip.props.uri, refclip.props.uri) + self.assertEqual(timeline.props.duration, self.timeline.props.duration) + + self.assertEqual(timeline.get_asset(), project) + self.assertEqual(len(project.list_assets(GES.Extractable)), 2) + + def test_nested_serialization(self): + nested_timeline = common.create_project(with_group=True, saved=True) + nested_project = nested_timeline.get_asset() + layer = nested_timeline.append_layer() + + asset = GES.UriClipAsset.request_sync(nested_project.props.id) + refclip = self.layer.add_asset(asset, 0, 0, 110 * Gst.SECOND, GES.TrackType.UNKNOWN) + nested_project.save(nested_timeline, nested_project.props.id, None, True) + + project = self.timeline.get_asset() + project.add_asset(nested_project) + uri = "file://%s" % tempfile.NamedTemporaryFile(suffix=".xges").name + self.assertEqual(len(project.list_assets(GES.Extractable)), 2) + project.save(self.timeline, uri, None, overwrite=True) + self.assertEqual(len(project.list_assets(GES.Extractable)), 2) + + mainloop = common.create_main_loop() + def loaded(unused_project, unused_timeline): + mainloop.quit() + project.connect("loaded", loaded) + + # Extract again the timeline and compare with previous one. + timeline = project.extract() + mainloop.run() + layer, = timeline.get_layers() + clip, = layer.get_clips() + self.assertEqual(clip.props.uri, refclip.props.uri) + self.assertEqual(timeline.props.duration, self.timeline.props.duration) + + self.assertEqual(timeline.get_asset(), project) + self.assertEqual(len(project.list_assets(GES.Extractable)), 2) + def test_timeline_duration(self): self.append_clip() self.append_clip()