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
{
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;

View file

@ -45,8 +45,9 @@ struct _GESBaseXmlFormatter
/*< public > */
/* <private> */
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,
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 *

View file

@ -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"

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
* 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);

View file

@ -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

View file

@ -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()