mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2025-01-24 16:18:16 +00:00
timeline: Better handle loading inconsistent timelines
Auto transition when having 3 overlapping clips in a same point in the timeline is not supported as we can't handle it in a nice way. Before we to avoid creating 2 overlapping transitions (which is plain broken in NLE) were completely disabling `auto-transition` and removing all auto-transitions in the timeline but this is pretty weird for the end user. This commit changes and now makes sure 2 transitions are not created in the same place. Also cleanup previous test case.
This commit is contained in:
parent
aa2f29bad3
commit
c750345c75
3 changed files with 107 additions and 33 deletions
|
@ -959,8 +959,18 @@ _create_auto_transition_from_transitions (GESTimeline * timeline,
|
||||||
_find_transition_from_auto_transitions (timeline, layer, track, prev,
|
_find_transition_from_auto_transitions (timeline, layer, track, prev,
|
||||||
next, transition_duration);
|
next, transition_duration);
|
||||||
|
|
||||||
if (auto_transition)
|
|
||||||
|
if (auto_transition) {
|
||||||
|
if (timeline->priv->needs_rollback) {
|
||||||
|
GST_WARNING_OBJECT (timeline,
|
||||||
|
"Created an auto transition where we have 3 overlapping clips"
|
||||||
|
" removing it as this case is NOT allowed nor supported");
|
||||||
|
g_signal_emit_by_name (auto_transition, "destroy-me");
|
||||||
|
timeline->priv->needs_rollback = FALSE;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
return auto_transition;
|
return auto_transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Try to find a transition that perfectly fits with the one that
|
/* Try to find a transition that perfectly fits with the one that
|
||||||
|
@ -2343,28 +2353,6 @@ layer_auto_transition_changed_cb (GESLayer * layer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g_list_free_full (clips, gst_object_unref);
|
g_list_free_full (clips, gst_object_unref);
|
||||||
|
|
||||||
if (timeline->priv->needs_rollback) {
|
|
||||||
GList *tmp, *trans;
|
|
||||||
|
|
||||||
ges_layer_set_auto_transition (layer, FALSE);
|
|
||||||
GST_ERROR_OBJECT (layer, "Has overlapping transition, "
|
|
||||||
" we can't handle that, setting auto_transition"
|
|
||||||
" to FALSE, and removing all transitions");
|
|
||||||
trans = g_list_copy (timeline->priv->auto_transitions);
|
|
||||||
for (tmp = trans; tmp; tmp = tmp->next) {
|
|
||||||
g_signal_emit_by_name (tmp->data, "destroy-me");
|
|
||||||
}
|
|
||||||
|
|
||||||
g_list_free (trans);
|
|
||||||
|
|
||||||
trans = ges_layer_get_clips (layer);
|
|
||||||
for (tmp = trans; tmp; tmp = tmp->next) {
|
|
||||||
if (GES_IS_TRANSITION_CLIP (tmp->data))
|
|
||||||
ges_layer_remove_clip (layer, tmp->data);
|
|
||||||
}
|
|
||||||
g_list_free_full (trans, gst_object_unref);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
|
@ -26,6 +26,8 @@ gi.require_version("GES", "1.0")
|
||||||
from gi.repository import Gst # noqa
|
from gi.repository import Gst # noqa
|
||||||
from gi.repository import GES # noqa
|
from gi.repository import GES # noqa
|
||||||
from gi.repository import GLib # noqa
|
from gi.repository import GLib # noqa
|
||||||
|
import contextlib # noqa
|
||||||
|
import os #noqa
|
||||||
import unittest # noqa
|
import unittest # noqa
|
||||||
import tempfile # noqa
|
import tempfile # noqa
|
||||||
|
|
||||||
|
@ -82,6 +84,23 @@ def create_project(with_group=False, saved=False):
|
||||||
return timeline
|
return timeline
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def created_project_file(xges):
|
||||||
|
_, xges_path = tempfile.mkstemp(suffix=".xges")
|
||||||
|
with open(xges_path, "w") as f:
|
||||||
|
f.write(xges)
|
||||||
|
|
||||||
|
yield Gst.filename_to_uri(os.path.abspath(xges_path))
|
||||||
|
|
||||||
|
os.remove(xges_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_asset_uri(name):
|
||||||
|
python_tests_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
assets_dir = os.path.join(python_tests_dir, "..", "assets")
|
||||||
|
return Gst.filename_to_uri(os.path.join(assets_dir, name))
|
||||||
|
|
||||||
|
|
||||||
class GESTest(unittest.TestCase):
|
class GESTest(unittest.TestCase):
|
||||||
|
|
||||||
def _log(self, func, format, *args):
|
def _log(self, func, format, *args):
|
||||||
|
|
|
@ -29,9 +29,7 @@ from gi.repository import GES # noqa
|
||||||
import unittest # noqa
|
import unittest # noqa
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from .common import create_main_loop
|
from . import common # noqa
|
||||||
from .common import create_project
|
|
||||||
from .common import GESSimpleTimelineTest # noqa
|
|
||||||
|
|
||||||
Gst.init(None)
|
Gst.init(None)
|
||||||
GES.init()
|
GES.init()
|
||||||
|
@ -40,8 +38,8 @@ GES.init()
|
||||||
class TestTimeline(unittest.TestCase):
|
class TestTimeline(unittest.TestCase):
|
||||||
|
|
||||||
def test_signals_not_emitted_when_loading(self):
|
def test_signals_not_emitted_when_loading(self):
|
||||||
mainloop = create_main_loop()
|
mainloop = common.create_main_loop()
|
||||||
timeline = create_project(with_group=True, saved=True)
|
timeline = common.create_project(with_group=True, saved=True)
|
||||||
|
|
||||||
# Reload the project, check the group.
|
# Reload the project, check the group.
|
||||||
project = GES.Project.new(uri=timeline.get_asset().props.uri)
|
project = GES.Project.new(uri=timeline.get_asset().props.uri)
|
||||||
|
@ -66,7 +64,7 @@ class TestTimeline(unittest.TestCase):
|
||||||
handle.assert_not_called()
|
handle.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
class TestSplitting(GESSimpleTimelineTest):
|
class TestSplitting(common.GESSimpleTimelineTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.track_types = [GES.TrackType.AUDIO]
|
self.track_types = [GES.TrackType.AUDIO]
|
||||||
super(TestSplitting, self).setUp()
|
super(TestSplitting, self).setUp()
|
||||||
|
@ -118,7 +116,7 @@ class TestSplitting(GESSimpleTimelineTest):
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
class TestEditing(GESSimpleTimelineTest):
|
class TestEditing(common.GESSimpleTimelineTest):
|
||||||
|
|
||||||
def test_transition_disappears_when_moving_to_another_layer(self):
|
def test_transition_disappears_when_moving_to_another_layer(self):
|
||||||
self.timeline.props.auto_transition = True
|
self.timeline.props.auto_transition = True
|
||||||
|
@ -176,7 +174,7 @@ class TestEditing(GESSimpleTimelineTest):
|
||||||
self.assertEquals(len(self.layer.get_clips()), 4)
|
self.assertEquals(len(self.layer.get_clips()), 4)
|
||||||
|
|
||||||
|
|
||||||
class TestSnapping(GESSimpleTimelineTest):
|
class TestSnapping(common.GESSimpleTimelineTest):
|
||||||
|
|
||||||
def test_snapping(self):
|
def test_snapping(self):
|
||||||
self.timeline.props.auto_transition = True
|
self.timeline.props.auto_transition = True
|
||||||
|
@ -216,7 +214,7 @@ class TestSnapping(GESSimpleTimelineTest):
|
||||||
self.assertEqual(clip1.props.duration, split_position)
|
self.assertEqual(clip1.props.duration, split_position)
|
||||||
self.assertEqual(clip2.props.start, split_position)
|
self.assertEqual(clip2.props.start, split_position)
|
||||||
|
|
||||||
class TestTransitions(GESSimpleTimelineTest):
|
class TestTransitions(common.GESSimpleTimelineTest):
|
||||||
|
|
||||||
def test_emission_order_for_transition_clip_added_signal(self):
|
def test_emission_order_for_transition_clip_added_signal(self):
|
||||||
self.timeline.props.auto_transition = True
|
self.timeline.props.auto_transition = True
|
||||||
|
@ -245,6 +243,75 @@ class TestTransitions(GESSimpleTimelineTest):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
signals, ["notify::start", "clip-added", "clip-added"])
|
signals, ["notify::start", "clip-added", "clip-added"])
|
||||||
|
|
||||||
|
def create_xges(self):
|
||||||
|
uri = common.get_asset_uri("png.png")
|
||||||
|
return """<ges version='0.4'>
|
||||||
|
<project properties='properties;' metadatas='metadatas, author=(string)"", render-scale=(double)100, format-version=(string)0.4;'>
|
||||||
|
<ressources>
|
||||||
|
<asset id='GESTitleClip' extractable-type-name='GESTitleClip' properties='properties;' metadatas='metadatas;' />
|
||||||
|
<asset id='bar-wipe-lr' extractable-type-name='GESTransitionClip' properties='properties;' metadatas='metadatas, description=(string)GES_VIDEO_STANDARD_TRANSITION_TYPE_BAR_WIPE_LR;' />
|
||||||
|
<asset id='%(uri)s' extractable-type-name='GESUriClip' properties='properties, supported-formats=(int)4, duration=(guint64)18446744073709551615;' metadatas='metadatas, video-codec=(string)PNG, file-size=(guint64)73294;' />
|
||||||
|
<asset id='crossfade' extractable-type-name='GESTransitionClip' properties='properties;' metadatas='metadatas, description=(string)GES_VIDEO_STANDARD_TRANSITION_TYPE_CROSSFADE;' />
|
||||||
|
</ressources>
|
||||||
|
<timeline properties='properties, auto-transition=(boolean)true, snapping-distance=(guint64)31710871;' metadatas='metadatas, duration=(guint64)13929667032;'>
|
||||||
|
<track caps='video/x-raw(ANY)' track-type='4' track-id='0' properties='properties, async-handling=(boolean)false, message-forward=(boolean)true, caps=(string)"video/x-raw\(ANY\)", restriction-caps=(string)"video/x-raw\,\ width\=\(int\)1920\,\ height\=\(int\)1080\,\ framerate\=\(fraction\)30/1", mixing=(boolean)true;' metadatas='metadatas;'/>
|
||||||
|
<track caps='audio/x-raw(ANY)' track-type='2' track-id='1' properties='properties, async-handling=(boolean)false, message-forward=(boolean)true, caps=(string)"audio/x-raw\(ANY\)", restriction-caps=(string)"audio/x-raw\,\ format\=\(string\)S32LE\,\ channels\=\(int\)2\,\ rate\=\(int\)44100\,\ layout\=\(string\)interleaved", mixing=(boolean)true;' metadatas='metadatas;'/>
|
||||||
|
<layer priority='0' properties='properties, auto-transition=(boolean)true;' metadatas='metadatas, volume=(float)1;'>
|
||||||
|
<clip id='0' asset-id='%(uri)s' type-name='GESUriClip' layer-priority='0' track-types='6' start='0' duration='4558919302' inpoint='0' rate='0' properties='properties, name=(string)uriclip25263, mute=(boolean)false, is-image=(boolean)false;' />
|
||||||
|
<clip id='1' asset-id='bar-wipe-lr' type-name='GESTransitionClip' layer-priority='0' track-types='4' start='3225722559' duration='1333196743' inpoint='0' rate='0' properties='properties, name=(string)transitionclip84;' children-properties='properties, GESVideoTransition::border=(uint)0, GESVideoTransition::invert=(boolean)false;'/>
|
||||||
|
<clip id='2' asset-id='%(uri)s' type-name='GESUriClip' layer-priority='0' track-types='6' start='3225722559' duration='3479110239' inpoint='4558919302' rate='0' properties='properties, name=(string)uriclip25265, mute=(boolean)false, is-image=(boolean)false;' />
|
||||||
|
</layer>
|
||||||
|
<layer priority='1' properties='properties, auto-transition=(boolean)true;' metadatas='metadatas, volume=(float)1;'>
|
||||||
|
<clip id='3' asset-id='%(uri)s' type-name='GESUriClip' layer-priority='1' track-types='4' start='8566459322' duration='1684610449' inpoint='0' rate='0' properties='properties, name=(string)uriclip25266, mute=(boolean)false, is-image=(boolean)true;' />
|
||||||
|
<clip id='4' asset-id='GESTitleClip' type-name='GESTitleClip' layer-priority='1' track-types='6' start='8566459322' duration='4500940746' inpoint='0' rate='0' properties='properties, name=(string)titleclip69;' />
|
||||||
|
<clip id='5' asset-id='%(uri)s' type-name='GESUriClip' layer-priority='1' track-types='4' start='9566459322' duration='4363207710' inpoint='0' rate='0' properties='properties, name=(string)uriclip25275, mute=(boolean)false, is-image=(boolean)true;' />
|
||||||
|
</layer>
|
||||||
|
<groups>
|
||||||
|
</groups>
|
||||||
|
</timeline>
|
||||||
|
</project>
|
||||||
|
</ges>""" % {"uri": uri}
|
||||||
|
|
||||||
|
def test_auto_transition(self):
|
||||||
|
xges = self.create_xges()
|
||||||
|
with common.created_project_file(xges) as proj_uri:
|
||||||
|
project = GES.Project.new(proj_uri)
|
||||||
|
timeline = project.extract()
|
||||||
|
|
||||||
|
mainloop = common.create_main_loop()
|
||||||
|
mainloop.run(until_empty=True)
|
||||||
|
|
||||||
|
layers = timeline.get_layers()
|
||||||
|
self.assertEqual(len(layers), 2)
|
||||||
|
|
||||||
|
self.assertTrue(layers[0].props.auto_transition)
|
||||||
|
self.assertTrue(layers[1].props.auto_transition)
|
||||||
|
|
||||||
|
def test_transition_type(self):
|
||||||
|
xges = self.create_xges()
|
||||||
|
with common.created_project_file(xges) as proj_uri:
|
||||||
|
project = GES.Project.new(proj_uri)
|
||||||
|
timeline = project.extract()
|
||||||
|
|
||||||
|
mainloop = common.create_main_loop()
|
||||||
|
mainloop.run(until_empty=True)
|
||||||
|
|
||||||
|
layers = timeline.get_layers()
|
||||||
|
self.assertEqual(len(layers), 2)
|
||||||
|
|
||||||
|
clips = layers[0].get_clips()
|
||||||
|
clip1 = clips[0]
|
||||||
|
clip2 = clips[-1]
|
||||||
|
# There should be a transition because clip1 intersects clip2
|
||||||
|
self.assertLess(clip1.props.start, clip2.props.start)
|
||||||
|
self.assertLess(clip2.props.start, clip1.props.start + clip1.props.duration)
|
||||||
|
self.assertLess(clip1.props.start + clip1.props.duration, clip2.props.start + clip2.props.duration)
|
||||||
|
self.assertEqual(len(clips), 3)
|
||||||
|
|
||||||
|
# Even though 3 clips overlap 1 transition will be created
|
||||||
|
clips = layers[1].get_clips()
|
||||||
|
self.assertEqual(len(clips), 4)
|
||||||
|
|
||||||
|
|
||||||
class TestPriorities(GESSimpleTimelineTest):
|
class TestPriorities(GESSimpleTimelineTest):
|
||||||
|
|
||||||
|
@ -257,4 +324,4 @@ class TestPriorities(GESSimpleTimelineTest):
|
||||||
|
|
||||||
clip.props.start = 101
|
clip.props.start = 101
|
||||||
self.timeline.commit()
|
self.timeline.commit()
|
||||||
self.assertGreater(clip.props.priority, clip1.props.priority)
|
self.assertGreater(clip.props.priority, clip1.props.priority)
|
||||||
|
|
Loading…
Reference in a new issue