ges: Implement subprojects

Subprojects simply consist of adding the GESProject
to the main project asset list. Then those are recursively
serialized in the main project in the <asset> not, when deserializing,
temporary files are created and those will be used in clips
as necessary
This commit is contained in:
Thibault Saunier 2019-07-07 20:55:53 -04:00
parent 5f3adbc1a3
commit a5fa2c35aa
8 changed files with 634 additions and 126 deletions

View file

@ -73,7 +73,6 @@ typedef enum
struct _GESBaseXmlFormatterPrivate struct _GESBaseXmlFormatterPrivate
{ {
GMarkupParseContext *parsecontext; GMarkupParseContext *parsecontext;
gchar *xmlcontent;
gsize xmlsize; gsize xmlsize;
LoadingState state; LoadingState state;
@ -139,6 +138,12 @@ G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GESBaseXmlFormatter,
static gint static gint
compare_assets_for_loading (PendingAsset * a, PendingAsset * b) 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) if (a->proxy_id)
return -1; return -1;
@ -157,7 +162,7 @@ _parse (GESBaseXmlFormatter * self, GError ** error, LoadingState state)
GES_BASE_XML_FORMATTER_GET_CLASS (self); GES_BASE_XML_FORMATTER_GET_CLASS (self);
GESBaseXmlFormatterPrivate *priv = _GET_PRIV (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, err = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED,
"Nothing contained in the project file."); "Nothing contained in the project file.");
@ -169,7 +174,7 @@ _parse (GESBaseXmlFormatter * self, GError ** error, LoadingState state)
priv->state = state; priv->state = state;
GST_DEBUG_OBJECT (self, "Running %s pass", loading_state_name (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)) priv->xmlsize, &err))
goto failed; goto failed;
@ -215,7 +220,8 @@ _load_and_parse (GESBaseXmlFormatter * self, const gchar * uri, GError ** error,
GError *err = NULL; 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); file = g_file_new_for_uri (uri);
/* TODO Handle GCancellable */ /* TODO Handle GCancellable */
@ -225,8 +231,8 @@ _load_and_parse (GESBaseXmlFormatter * self, const gchar * uri, GError ** error,
goto failed; goto failed;
} }
g_clear_pointer (&priv->xmlcontent, g_free); g_clear_pointer (&self->xmlcontent, g_free);
if (!g_file_load_contents (file, NULL, &priv->xmlcontent, &priv->xmlsize, if (!g_file_load_contents (file, NULL, &self->xmlcontent, &priv->xmlsize,
NULL, &err)) NULL, &err))
goto failed; goto failed;
g_object_unref (file); g_object_unref (file);
@ -374,11 +380,12 @@ _dispose (GObject * object)
static void static void
_finalize (GObject * object) _finalize (GObject * object)
{ {
GESBaseXmlFormatter *self = GES_BASE_XML_FORMATTER (object);
GESBaseXmlFormatterPrivate *priv = _GET_PRIV (object); GESBaseXmlFormatterPrivate *priv = _GET_PRIV (object);
if (priv->parsecontext != NULL) if (priv->parsecontext != NULL)
g_markup_parse_context_free (priv->parsecontext); 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); g_list_free_full (priv->groups, (GDestroyNotify) _free_pending_group);
priv->groups = NULL; priv->groups = NULL;

View file

@ -45,8 +45,9 @@ struct _GESBaseXmlFormatter
/*< public > */ /*< public > */
/* <private> */ /* <private> */
GESBaseXmlFormatterPrivate *priv; GESBaseXmlFormatterPrivate *priv;
gchar *xmlcontent;
gpointer _ges_reserved[GES_PADDING]; gpointer _ges_reserved[GES_PADDING - 1];
}; };
/** /**

View file

@ -326,6 +326,12 @@ G_GNUC_INTERNAL void ges_base_xml_formatter_add_control_binding (GESBaseXmlForma
const gchar *track_id, const gchar *track_id,
GSList * timed_values); 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, G_GNUC_INTERNAL gboolean set_property_foreach (GQuark field_id,
const GValue * value, const GValue * value,
GObject * object); GObject * object);
@ -345,11 +351,6 @@ G_GNUC_INTERNAL gint element_end_compare (GESTimelineElement *
G_GNUC_INTERNAL GstElementFactory * G_GNUC_INTERNAL GstElementFactory *
ges_get_compositor_factory (void); 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 * * GESContainer *

View file

@ -43,6 +43,24 @@
* It lets you request new asset, and it informs you about new assets through * 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 * a set of signals. Also it handles problem such as missing files/missing
* #GstElement and lets you try to recover from those. * #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 #ifdef HAVE_CONFIG_H
#include "config.h" #include "config.h"

File diff suppressed because it is too large Load diff

View file

@ -26,13 +26,6 @@
* GES needs to be initialized after GStreamer itself. This section * GES needs to be initialized after GStreamer itself. This section
* contains the various functions to do so. * 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 #ifdef HAVE_CONFIG_H
#include "config.h" #include "config.h"
@ -229,6 +222,7 @@ ges_deinit (void)
g_type_class_unref (g_type_class_peek (GES_TYPE_EFFECT)); g_type_class_unref (g_type_class_peek (GES_TYPE_EFFECT));
ges_asset_cache_deinit (); ges_asset_cache_deinit ();
ges_xml_formatter_deinit ();
initialized_thread = NULL; initialized_thread = NULL;
G_UNLOCK (init_lock); G_UNLOCK (init_lock);

View file

@ -62,24 +62,27 @@ def create_main_loop():
def create_project(with_group=False, saved=False): def create_project(with_group=False, saved=False):
"""Creates a project with two clips in a group.""" """Creates a project with two clips in a group."""
project = GES.Project.new(None) timeline = GES.Timeline.new_audio_video()
timeline = project.extract()
layer = timeline.append_layer() layer = timeline.append_layer()
if with_group: if with_group:
clip1 = GES.TitleClip() clip1 = GES.TitleClip()
clip1.set_start(0) clip1.set_start(0)
clip1.set_duration(10) clip1.set_duration(10*Gst.SECOND)
layer.add_clip(clip1) layer.add_clip(clip1)
clip2 = GES.TitleClip() clip2 = GES.TitleClip()
clip2.set_start(100) clip2.set_start(100 * Gst.SECOND)
clip2.set_duration(10) clip2.set_duration(10*Gst.SECOND)
layer.add_clip(clip2) layer.add_clip(clip2)
group = GES.Container.group([clip1, clip2]) group = GES.Container.group([clip1, clip2])
if saved: if saved:
uri = "file://%s" % tempfile.NamedTemporaryFile(suffix=".xges").name if isinstance(saved, str):
project.save(timeline, uri, None, overwrite=True) 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 return timeline

View file

@ -19,6 +19,7 @@
from . import overrides_hack from . import overrides_hack
import tempfile # noqa
import gi import gi
gi.require_version("Gst", "1.0") gi.require_version("Gst", "1.0")
@ -45,7 +46,6 @@ class TestTimeline(common.GESSimpleTimelineTest):
project = GES.Project.new(uri=timeline.get_asset().props.uri) project = GES.Project.new(uri=timeline.get_asset().props.uri)
loaded_called = False loaded_called = False
def loaded(unused_project, unused_timeline): def loaded(unused_project, unused_timeline):
nonlocal loaded_called nonlocal loaded_called
loaded_called = True loaded_called = True
@ -63,6 +63,77 @@ class TestTimeline(common.GESSimpleTimelineTest):
self.assertTrue(loaded_called) self.assertTrue(loaded_called)
handle.assert_not_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): def test_timeline_duration(self):
self.append_clip() self.append_clip()
self.append_clip() self.append_clip()