diff --git a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json index e5568e8f4..2889bda6f 100644 --- a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json +++ b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json @@ -1,18 +1,5 @@ { "OTIO_SCHEMA" : "PluginManifest.1", - "adapters": [ - { - "OTIO_SCHEMA": "Adapter.1", - "name": "xges", - "filepath": "xges.py", - "suffixes": ["xges"] - } - ], - "schemadefs" : [ - { - "OTIO_SCHEMA" : "SchemaDef.1", - "name" : "xges", - "filepath" : "xges.py" - } - ] + "adapters": [], + "schemadefs" : [] } diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/xges_example.xges b/contrib/opentimelineio_contrib/adapters/tests/sample_data/xges_example.xges deleted file mode 100644 index 4c6af0d68..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/xges_example.xges +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/tests_xges_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/tests_xges_adapter.py deleted file mode 100644 index 127e9f857..000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/tests_xges_adapter.py +++ /dev/null @@ -1,2695 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright Contributors to the OpenTimelineIO project - -import os -import tempfile -import unittest -from fractions import Fraction -from xml.etree import ElementTree - -import opentimelineio as otio -import opentimelineio.test_utils as otio_test_utils -from opentimelineio.schema import ( - Timeline, - Stack, - Track, - Transition, - Clip, - Gap, - ExternalReference, - TrackKind, - Effect, - Marker, - MarkerColor) - -SAMPLE_DATA_DIR = os.path.join(os.path.dirname(__file__), "sample_data") -XGES_EXAMPLE_PATH = os.path.join(SAMPLE_DATA_DIR, "xges_example.xges") -XGES_TIMING_PATH = os.path.join(SAMPLE_DATA_DIR, "xges_timing_example.xges") -XGES_NESTED_PATH = os.path.join(SAMPLE_DATA_DIR, "xges_nested_example.xges") -IMAGE_SEQUENCE_EXAMPLE_PATH = os.path.join( - SAMPLE_DATA_DIR, "image_sequence_example.otio") - -SCHEMA = otio.schema.schemadef.module_from_name("xges") -# TODO: remove once python2 has ended: -# (problem is that python2 needs a source code encoding -# definition to include utf8 text!!!) -if str is bytes: - UTF8_NAME = 'Ri"\',;=)(+9@{\xcf\x93\xe7\xb7\xb6\xe2\x98\xba'\ - '\xef\xb8\x8f l\xd1\xa6\xf1\xbd\x9a\xbb\xf1\xa6\x84\x83 \\' -else: - UTF8_NAME = str( - b'Ri"\',;=)(+9@{\xcf\x93\xe7\xb7\xb6\xe2\x98\xba\xef\xb8' - b'\x8f l\xd1\xa6\xf1\xbd\x9a\xbb\xf1\xa6\x84\x83 \\', - encoding="utf8") -GST_SECOND = 1000000000 - - -def _rat_tm_from_secs(val, rate=25.0): - """Return a RationalTime for the given timestamp (in seconds).""" - return otio.opentime.from_seconds(val).rescaled_to(rate) - - -def _tm_range_from_secs(start, dur, rate=25.0): - """ - Return a TimeRange for the given timestamp and duration (in - seconds). - """ - return otio.opentime.TimeRange( - _rat_tm_from_secs(start), _rat_tm_from_secs(dur)) - - -def _make_media_ref(uri="file:///example", start=0, duration=1, name=""): - """Return an ExternalReference.""" - ref = ExternalReference( - target_url=uri, - available_range=_tm_range_from_secs(start, duration)) - ref.name = name - return ref - - -def _make_clip(uri="file:///example", start=0, duration=1, name=""): - """Return a Clip.""" - ref = _make_media_ref(uri, start, duration) - return Clip(name=name, media_reference=ref) - - -def _add_marker(otio_item, name, color, start, duration): - """Add a marker to an otio item""" - otio_item.markers.append(Marker( - name=name, color=color, - marked_range=_tm_range_from_secs(start, duration))) - - -def _make_ges_marker( - position, otio_color=None, comment=None, metadatas=None): - """ - Return a GESMarker with the given timeline position (in seconds). - """ - if comment is not None: - metadatas = metadatas or SCHEMA.GstStructure("metadatas") - metadatas.set("comment", "string", comment) - ges_marker = SCHEMA.GESMarker(position * GST_SECOND, metadatas) - if otio_color is not None: - ges_marker.set_color_from_otio_color(otio_color) - return ges_marker - - -class XgesElement: - """ - Generates an xges string to be converted to an otio timeline. - """ - - def __init__(self, name=None, marker_list=None): - self.ges = ElementTree.Element("ges") - self.project = ElementTree.SubElement(self.ges, "project") - if name is not None: - self.project.attrib["metadatas"] = \ - "metadatas, name=(string){};".format( - SCHEMA.GstStructure.serialize_string(name)) - self.ressources = ElementTree.SubElement( - self.project, "ressources") - self.timeline = ElementTree.SubElement( - self.project, "timeline") - if marker_list is not None: - self.timeline.attrib["metadatas"] = \ - "metadatas, markers=(GESMarkerList){};".format( - SCHEMA.GstStructure.serialize_marker_list(marker_list)) - self.layer_priority = 0 - self.track_id = 0 - self.clip_id = 0 - self.layer = None - self.clip = None - - def add_audio_track(self): - """Add a basic Audio track.""" - track = ElementTree.SubElement( - self.timeline, "track", { - "caps": "audio/x-raw(ANY)", - "track-type": "2", - "track-id": str(self.track_id), - "properties": - r'properties, restriction-caps=(string)' - r'"audio/x-raw\,\ format\=\(string\)S32LE\,\ ' - r'channels\=\(int\)2\,\ rate\=\(int\)44100\,\ ' - r'layout\=\(string\)interleaved", ' - r'mixing=(boolean)true;'}) - self.track_id += 1 - return track - - def add_video_track(self, framerate=None): - """Add a basic Video track.""" - res_caps = \ - r"video/x-raw\,\ width\=\(int\)300\,\ height\=\(int\)250" - if framerate: - res_caps += fr"\,\ framerate\=\(fraction\){framerate}" - track = ElementTree.SubElement( - self.timeline, "track", { - "caps": "video/x-raw(ANY)", - "track-type": "4", - "track-id": str(self.track_id), - "properties": - 'properties, restriction-caps=(string)' - '"{}", mixing=(boolean)true;'.format(res_caps)}) - self.track_id += 1 - return track - - def add_text_track(self): - """Add a basic Audio track.""" - track = ElementTree.SubElement( - self.timeline, "track", { - "caps": "text/x-raw(ANY)", - "track-type": "8", - "track-id": str(self.track_id), - "properties": - 'properties, mixing=(boolean)false;'}) - self.track_id += 1 - return track - - def add_layer(self): - """Append a (lower priority) layer to the timeline.""" - self.layer = ElementTree.SubElement( - self.timeline, "layer", - {"priority": str(self.layer_priority)}) - self.layer_priority += 1 - return self.layer - - def add_asset(self, asset_id, extract_type, duration=None): - """Add an asset to the project if it does not already exist.""" - asset = self.ressources.find( - "./asset[@id='{}'][@extractable-type-name='{}']".format( - asset_id, extract_type)) - if asset is not None: - return asset - asset = ElementTree.SubElement( - self.ressources, "asset", - {"id": asset_id, "extractable-type-name": extract_type}) - if duration is not None: - asset.attrib["properties"] = \ - "properties, duration=(guint64){:d};".format( - duration * GST_SECOND) - return asset - - def add_clip( - self, start, duration, inpoint, type_name, track_types, - asset_id=None, name=None, asset_duration=None, - properties=None, metadatas=None): - """Add a clip to the most recent layer.""" - layer_priority = self.layer.get("priority") - if asset_id is None: - if type_name == "GESUriClip": - asset_id = "file:///example" - elif type_name == "GESTransitionClip": - asset_id = "crossfade" - else: - asset_id = type_name - if asset_duration is None and type_name == "GESUriClip": - asset_duration = 100 - self.clip = ElementTree.SubElement( - self.layer, "clip", { - "id": str(self.clip_id), - "asset-id": asset_id, - "type-name": type_name, - "track-types": str(track_types), - "layer-priority": str(layer_priority), - "start": str(start * GST_SECOND), - "inpoint": str(inpoint * GST_SECOND), - "duration": str(duration * GST_SECOND)}) - self.add_asset(asset_id, type_name, asset_duration) - if properties is not None: - self.clip.attrib["properties"] = str(properties) - if metadatas is not None: - self.clip.attrib["metadatas"] = str(metadatas) - if name is not None: - if properties is None: - properties = SCHEMA.GstStructure("properties") - properties.set("name", "string", name) - self.clip.attrib["properties"] = str(properties) - self.clip_id += 1 - return self.clip - - def add_effect( - self, effect_name, track_type, track_id, - type_name=None, properties=None, metadatas=None, - children_properties=None): - """Add an effect to the most recent clip.""" - if type_name is None: - type_name = "GESEffect" - clip_id = self.clip.get("id") - effect = ElementTree.SubElement( - self.clip, "effect", { - "asset-id": effect_name, - "clip-id": str(clip_id), - "type-name": type_name, - "track-type": str(track_type), - "track-id": str(track_id)}) - if properties is not None: - effect.attrib["properties"] = str(properties) - if metadatas is not None: - effect.attrib["metadatas"] = str(metadatas) - if children_properties is not None: - effect.attrib["children-properties"] = str( - children_properties) - return effect - - def get_otio_timeline(self): - """Return a Timeline using otio's read_from_string method.""" - string = ElementTree.tostring(self.ges, encoding="UTF-8") - return otio.adapters.read_from_string(string, "xges") - - -class CustomOtioAssertions: - """Custom Assertions to perform on otio objects""" - - @staticmethod - def _typed_name(otio_obj): - name = otio_obj.name - if not name: - name = '""' - return f"{otio_obj.schema_name()} {name}" - - @classmethod - def _otio_id(cls, otio_obj): - otio_id = cls._typed_name(otio_obj) - if isinstance(otio_obj, otio.core.Composable): - otio_parent = otio_obj.parent() - if otio_parent is None: - otio_id += " (No Parent)" - else: - index = otio_parent.index(otio_obj) - otio_id += " (Child {:d} of {})".format( - index, cls._typed_name(otio_parent)) - return otio_id - - @staticmethod - def _tm(rat_tm): - return "{:g}/{:g}({:g}s)".format( - rat_tm.value, rat_tm.rate, rat_tm.value / rat_tm.rate) - - @classmethod - def _range(cls, tm_range): - return "start_time:" + cls._tm(tm_range.start_time) \ - + ", duration:" + cls._tm(tm_range.duration) - - @classmethod - def _val_str(cls, val): - if isinstance(val, otio.opentime.RationalTime): - return cls._tm(val) - if isinstance(val, otio.opentime.TimeRange): - return cls._range(val) - return str(val) - - def assertOtioHasAttr(self, otio_obj, attr_name): - """Assert that the otio object has an attribute.""" - if not hasattr(otio_obj, attr_name): - raise AssertionError( - "{} has no attribute {}".format( - self._otio_id(otio_obj), attr_name)) - - def assertOtioAttrIsNone(self, otio_obj, attr_name): - """Assert that the otio object attribute is None.""" - self.assertOtioHasAttr(otio_obj, attr_name) - val = getattr(otio_obj, attr_name) - if val is not None: - raise AssertionError( - "{} {}: {} is not None".format( - self._otio_id(otio_obj), attr_name, - self._val_str(val))) - - def assertOtioHasAttrPath(self, otio_obj, attr_path): - """ - Assert that the otio object has the attribute: - attr_path[0].attr_path[1].---.attr_path[-1] - and returns the value and an attribute string. - If an attribute is callable, it will be called (with no - arguments) before returning. - If an int is given in the attribute path, it will be treated as - a list index to call. - """ - first = True - attr_str = "" - val = otio_obj - for attr_name in attr_path: - if isinstance(attr_name, int): - if not hasattr(val, "__getitem__"): - raise AssertionError( - "{}{} is not a list".format( - self._otio_id(otio_obj), attr_str)) - try: - val = val[attr_name] - except Exception as err: - raise AssertionError( - "{}{}: can't access item {:d}:\n{!s}".format( - self._otio_id(otio_obj), attr_str, - attr_name, err)) - if first: - first = False - attr_str += " " - attr_str += f"[{attr_name:d}]" - else: - if not hasattr(val, attr_name): - raise AssertionError( - "{}{} has no attribute {}".format( - self._otio_id(otio_obj), attr_str, attr_name)) - val = getattr(val, attr_name) - if first: - first = False - attr_str += " " + attr_name - else: - attr_str += "." + attr_name - if callable(val): - val = val() - return val, attr_str - - def assertOtioAttrPathEqual(self, otio_obj, attr_path, compare): - """ - Assert that the otio object has the attribute: - attr_path[0].attr_path[1].---.attr_path[-1] - equal to 'compare'. - See assertOtioHasAttrPath for special cases for the attr_path. - """ - val, attr_str = self.assertOtioHasAttrPath(otio_obj, attr_path) - if val != compare: - raise AssertionError( - "{}{}: {} != {}".format( - self._otio_id(otio_obj), attr_str, - self._val_str(val), self._val_str(compare))) - - def assertOtioAttrPathEqualList( - self, otio_obj, list_path, attr_path, compare_list): - """ - Assert that the otio object has the attribute: - list_path[0].---.list_path[-1][i] - .attr_path[0].---.attr_path[-1] - == compare_list[i] - See assertOtioHasAttrPath for special cases for the attr_path - and list_path. - """ - _list, list_str = self.assertOtioHasAttrPath(otio_obj, list_path) - try: - num = len(_list) - except Exception as err: - raise AssertionError( - "{}{} has no len:\n{!s}".format( - self._otio_id(otio_obj), list_str, err)) - num_cmp = len(compare_list) - if num != num_cmp: - raise AssertionError( - "{}{} has a length of {:d} != {:d}".format( - self._otio_id(otio_obj), list_str, num, num_cmp)) - for index, compare in enumerate(compare_list): - self.assertOtioAttrPathEqual( - otio_obj, list_path + [index] + attr_path, compare) - - def assertOtioAttrEqual(self, otio_obj, attr_name, compare): - """ - Assert that the otio object attribute is equal to 'compare'. - If an attribute is callable, it will be called (with no - arguments) before comparing. - """ - self.assertOtioAttrPathEqual(otio_obj, [attr_name], compare) - - def assertOtioIsInstance(self, otio_obj, otio_class): - """ - Assert that the otio object is an instance of the given class. - """ - if not isinstance(otio_obj, otio_class): - raise AssertionError( - "{} is not an otio {} instance".format( - self._otio_id(otio_obj), otio_class.__name__)) - - def assertOtioAttrIsInstance(self, otio_obj, attr_name, otio_class): - """ - Assert that the otio object attribute is an instance of the - given class. - """ - self.assertOtioHasAttr(otio_obj, attr_name) - val = getattr(otio_obj, attr_name) - if not isinstance(val, otio_class): - raise AssertionError( - "{} {} is not an otio {} instance".format( - self._otio_id(otio_obj), attr_name, - otio_class.__name__)) - - def assertOtioOffsetTotal(self, otio_trans, compare): - """ - Assert that the Transition has a certain total offset. - """ - in_set = otio_trans.in_offset - out_set = otio_trans.out_offset - if in_set + out_set != compare: - raise AssertionError( - "{} in_offset + out_offset: {} + {} != {}".format( - self._otio_id(otio_trans), - self._val_str(in_set), self._val_str(out_set), - self._val_str(compare))) - - def assertOtioNumChildren(self, otio_obj, compare): - """ - Assert that the otio object has a certain number of children. - """ - self.assertOtioIsInstance(otio_obj, otio.core.Composable) - num = len(otio_obj) - if num != compare: - raise AssertionError( - "{} has {:d} children != {}".format( - self._otio_id(otio_obj), num, - self._val_str(compare))) - - -class OtioTest: - """Tests to be used by OtioTestNode and OtioTestTree.""" - - @staticmethod - def none_source(inst, otio_item): - """Test that the source_range is None.""" - inst.assertOtioAttrIsNone(otio_item, "source_range") - - @staticmethod - def is_audio(inst, otio_track): - """Test that a Track is Audio.""" - inst.assertOtioAttrEqual(otio_track, "kind", TrackKind.Audio) - - @staticmethod - def is_video(inst, otio_track): - """Test that a Track is Video.""" - inst.assertOtioAttrEqual(otio_track, "kind", TrackKind.Video) - - @staticmethod - def has_ex_ref(inst, otio_clip): - """Test that a clip has an ExternalReference.""" - inst.assertOtioAttrIsInstance( - otio_clip, "media_reference", ExternalReference) - - @staticmethod - def no_effects(inst, otio_item): - """Test that an item has no effects.""" - inst.assertOtioAttrPathEqualList(otio_item, ["effects"], [], []) - - @staticmethod - def no_markers(inst, otio_item): - """Test that an item has no markers.""" - inst.assertOtioAttrPathEqualList(otio_item, ["markers"], [], []) - - @staticmethod - def start_time(start): - """ - Return an equality test for an Item's source_range.start_time. - Argument should be a timestamp in seconds. - """ - return lambda inst, otio_item: inst.assertOtioAttrPathEqual( - otio_item, ["source_range", "start_time"], - _rat_tm_from_secs(start)) - - @staticmethod - def duration(dur): - """ - Return an equality test for an Item's source_range.duration. - Argument should be a timestamp in seconds. - """ - return lambda inst, otio_item: inst.assertOtioAttrPathEqual( - otio_item, ["source_range", "duration"], - _rat_tm_from_secs(dur)) - - @staticmethod - def _test_both_rate(inst, otio_item, _rate): - inst.assertOtioAttrPathEqual( - otio_item, ["source_range", "start_time", "rate"], _rate) - inst.assertOtioAttrPathEqual( - otio_item, ["source_range", "duration", "rate"], _rate) - - @classmethod - def rate(cls, _rate): - """ - Return an equality test for an Item's - source_range.start_time.rate and source_range.duration.rate. - """ - return lambda inst, otio_item: cls._test_both_rate( - inst, otio_item, _rate) - - @staticmethod - def range(start, dur): - """ - Return an equality test for an Item's source_range. - Arguments should be timestamps in seconds. - """ - return lambda inst, otio_item: inst.assertOtioAttrEqual( - otio_item, "source_range", _tm_range_from_secs(start, dur)) - - @staticmethod - def range_in_parent(start, dur): - """ - Return an equality test for an Item's range_in_parent(). - Arguments should be timestamps in seconds. - """ - return lambda inst, otio_item: inst.assertOtioAttrEqual( - otio_item, "range_in_parent", _tm_range_from_secs(start, dur)) - - @staticmethod - def offset_total(total): - """ - Return an equality test for a Transition's total offset/range. - Argument should be a timestamp in seconds. - """ - return lambda inst, otio_trans: inst.assertOtioOffsetTotal( - otio_trans, _rat_tm_from_secs(total)) - - @staticmethod - def name(name): - """Return an equality test for an Otio Object's name.""" - return lambda inst, otio_item: inst.assertOtioAttrEqual( - otio_item, "name", name) - - @staticmethod - def effects(*effect_names): - """Return a test that the otio_item contains the effects""" - return lambda inst, otio_item: inst.assertOtioAttrPathEqualList( - otio_item, ["effects"], ["effect_name"], list(effect_names)) - - @staticmethod - def _test_marker_details(inst, otio_item, marker_details): - inst.assertOtioAttrPathEqualList( - otio_item, ["markers"], ["name"], - [mrk["name"] for mrk in marker_details]) - inst.assertOtioAttrPathEqualList( - otio_item, ["markers"], ["color"], - [mrk["color"] for mrk in marker_details]) - inst.assertOtioAttrPathEqualList( - otio_item, ["markers"], ["marked_range", "start_time"], - [_rat_tm_from_secs(mrk["start"]) for mrk in marker_details]) - inst.assertOtioAttrPathEqualList( - otio_item, ["markers"], ["marked_range", "duration"], - [_rat_tm_from_secs(mrk["duration"]) for mrk in marker_details]) - - @classmethod - def markers(cls, *marker_details): - """ - Return a test that the otio_item contains the markers specified by - the marker_details, which are dictionaries with the keys: - color: (the marker color), - name: (the name of the marker), - start: (the start time of the marker in seconds), - duration: (the range of the marker in seconds) - """ - return lambda inst, otio_item: cls._test_marker_details( - inst, otio_item, marker_details) - - -class OtioTestNode: - """ - An OtioTestTree Node that corresponds to some expected otio class. - This holds information about the children of the node, as well as - a list of additional tests to perform on the corresponding otio - object. These tests should come from OtioTest. - """ - - def __init__(self, expect_type, children=[], tests=[]): - if expect_type is Timeline: - if len(children) != 1: - raise ValueError("A Timeline must have one child") - elif not issubclass(expect_type, otio.core.Composition): - if children: - raise ValueError( - "No children are allowed if not a Timeline or " - "Composition type") - self.expect_type = expect_type - self.children = children - self.tests = tests - - -class OtioTestTree: - """ - Test an otio object has the correct type structure, and perform - additional tests along the way.""" - - def __init__(self, unittest_inst, base, type_tests=None): - """ - First argument is a unittest instance which will perform all - tests. - 'type_test' argument is a dictionary of classes who's values are a - list of tests to perform whenever a node is found that is an - instance of that class. These tests should come from OtioTest. - 'base' argument is the base OtioTestNode, where the comparison - will begin. - """ - self.unittest_inst = unittest_inst - if type_tests is None: - self.type_tests = {} - else: - self.type_tests = type_tests - self.base = base - - def test_compare(self, otio_obj): - """ - Test that the given otio object has the expected tree structure - and run all tests that are found. - """ - self._sub_test_compare(otio_obj, self.base) - - def _sub_test_compare(self, otio_obj, node): - self.unittest_inst.assertOtioIsInstance( - otio_obj, node.expect_type) - if isinstance(otio_obj, Timeline): - self._sub_test_compare(otio_obj.tracks, node.children[0]) - elif isinstance(otio_obj, otio.core.Composition): - self.unittest_inst.assertOtioNumChildren( - otio_obj, len(node.children)) - for sub_obj, child in zip(otio_obj, node.children): - self._sub_test_compare(sub_obj, child) - for otio_type in self.type_tests: - if isinstance(otio_obj, otio_type): - for test in self.type_tests[otio_type]: - test(self.unittest_inst, otio_obj) - for test in node.tests: - test(self.unittest_inst, otio_obj) - - -class CustomXgesAssertions: - """Custom Assertions to perform on a ges xml object""" - - @staticmethod - def _xges_id(xml_el): - xges_id = f"Element <{xml_el.tag}" - for key, val in xml_el.attrib.items(): - xges_id += f" {key}='{val}'" - xges_id += " /> " - return xges_id - - def assertXgesNumElementsAtPath(self, xml_el, path, compare): - """ - Assert that the xml element has a certain number of descendants - at the given xml path. - Returns the matching descendants. - """ - found = xml_el.findall(path) or [] - num = len(found) - if num != compare: - raise AssertionError( - "{}Number of elements found at path {}: " - "{:d} != {:d}".format( - self._xges_id(xml_el), path, num, compare)) - return found - - def assertXgesOneElementAtPath(self, xml_el, path): - """ - Assert that the xml element has exactly one descendants at the - given xml path. - Returns the matching descendent. - """ - return self.assertXgesNumElementsAtPath(xml_el, path, 1)[0] - - def assertXgesHasTag(self, xml_el, tag): - """Assert that the xml element has a certain tag.""" - if xml_el.tag != tag: - raise AssertionError( - "{}does not have the tag {}".format( - self._xges_id(xml_el), tag)) - - def assertXgesHasAttr(self, xml_el, attr_name): - """ - Assert that the xml element has a certain attribute. - Returns its value. - """ - if attr_name not in xml_el.attrib: - raise AssertionError( - "{}has no attribute {}".format( - self._xges_id(xml_el), attr_name)) - return xml_el.attrib[attr_name] - - def assertXgesHasAllAttrs(self, xml_el, *attr_names): - """ - Assert that the xml element has all given attributes. - """ - for attr_name in attr_names: - self.assertXgesHasAttr(xml_el, attr_name) - - def assertXgesNumElementsAtPathWithAttr( - self, xml_el, path_base, attrs, compare): - """ - Assert that the xml element has a certain number of descendants - at the given xml path with the given attributes. - Returns the matching descendants. - """ - path = path_base - for key, val in attrs.items(): - if key in ("start", "duration", "inpoint"): - val *= GST_SECOND - path += f"[@{key}='{val!s}']" - return self.assertXgesNumElementsAtPath(xml_el, path, compare) - - def assertXgesOneElementAtPathWithAttr( - self, xml_el, path_base, attrs): - """ - Assert that the xml element has exactly one descendants at the - given xml path with the given attributes. - Returns the matching descendent. - """ - return self.assertXgesNumElementsAtPathWithAttr( - xml_el, path_base, attrs, 1)[0] - - def assertXgesIsGesElement(self, ges_el): - """ - Assert that the xml element has the expected basic structure of - a ges element. - """ - self.assertXgesHasTag(ges_el, "ges") - project = self.assertXgesOneElementAtPath(ges_el, "./project") - self.assertXgesHasAllAttrs(project, "properties", "metadatas") - self.assertXgesOneElementAtPath(ges_el, "./project/ressources") - timeline = self.assertXgesOneElementAtPath( - ges_el, "./project/timeline") - self.assertXgesHasAllAttrs(timeline, "properties", "metadatas") - - def assertXgesAttrEqual(self, xml_el, attr_name, compare): - """ - Assert that the xml element's attribute is equal to 'compare'. - """ - val = self.assertXgesHasAttr(xml_el, attr_name) - compare = str(compare) - if val != compare: - raise AssertionError( - "{}attribute {}: {} != {}".format( - self._xges_id(xml_el), attr_name, val, compare)) - - def assertXgesHasInStructure( - self, xml_el, struct_name, field_name, field_type): - """ - Assert that the xml element has a GstStructure attribute that - contains the given field. - Returns the value. - """ - struct = self.assertXgesHasAttr(xml_el, struct_name) - struct = SCHEMA.GstStructure.new_from_str(struct) - if field_name not in struct.fields: - raise AssertionError( - "{}attribute {} does not contain the field {}".format( - self._xges_id(xml_el), struct_name, field_name)) - if struct.get_type_name(field_name) != field_type: - raise AssertionError( - "{}attribute {}'s field {} is not of the type {}".format( - self._xges_id(xml_el), struct_name, field_name, - field_type)) - return struct[field_name] - - def assertXgesHasProperty(self, xml_el, prop_name, prop_type): - """ - Assert that the xml element has the given property. - Returns the value. - """ - return self.assertXgesHasInStructure( - xml_el, "properties", prop_name, prop_type) - - def assertXgesHasMetadata(self, xml_el, meta_name, meta_type): - """ - Assert that the xml element has the given metadata. - Returns the value. - """ - return self.assertXgesHasInStructure( - xml_el, "metadatas", meta_name, meta_type) - - def assertXgesStructureFieldEqual( - self, xml_el, struct_name, field_name, field_type, compare): - """ - Assert that a certain xml element structure field is equal to - 'compare'. - """ - val = self.assertXgesHasInStructure( - xml_el, struct_name, field_name, field_type) - # TODO: remove once python2 has ended - if field_type == "string": - if type(val) is not str and isinstance(val, str): - val = val.encode("utf8") - if isinstance(val, otio.core.SerializableObject): - equal = val.is_equivalent_to(compare) - else: - equal = val == compare - if not equal: - raise AssertionError( - "{}{} {}:\n{!s}\n!=\n{!s}".format( - self._xges_id(xml_el), struct_name, field_name, - val, compare)) - - def assertXgesPropertyEqual( - self, xml_el, prop_name, prop_type, compare): - """ - Assert that a certain xml element property is equal to - 'compare'. - """ - self.assertXgesStructureFieldEqual( - xml_el, "properties", prop_name, prop_type, compare) - - def assertXgesMetadataEqual( - self, xml_el, meta_name, meta_type, compare): - """ - Assert that a certain xml element metadata is equal to - 'compare'. - """ - self.assertXgesStructureFieldEqual( - xml_el, "metadatas", meta_name, meta_type, compare) - - def assertXgesStructureEqual(self, xml_el, attr_name, compare): - """ - Assert that the xml element structure is equal to 'compare'. - """ - struct = self.assertXgesHasAttr(xml_el, attr_name) - struct = SCHEMA.GstStructure.new_from_str(struct) - if not isinstance(compare, SCHEMA.GstStructure): - compare = SCHEMA.GstStructure.new_from_str(compare) - if not struct.is_equivalent_to(compare): - raise AssertionError( - "{}{}:\n{!r}\n!=\n{!r}".format( - self._xges_id(xml_el), attr_name, struct, compare)) - - def assertXgesTrackTypes(self, ges_el, *track_types): - """ - Assert that the ges element contains one track for each given - track type, and no more. - Returns the tracks in the same order as the types. - """ - tracks = [] - for track_type in track_types: - track = self.assertXgesOneElementAtPathWithAttr( - ges_el, "./project/timeline/track", - {"track-type": str(track_type)}) - self.assertXgesHasAllAttrs( - track, "caps", "track-type", "track-id", - "properties", "metadatas") - tracks.append(track) - self.assertXgesNumElementsAtPath( - ges_el, "./project/timeline/track", len(track_types)) - return tracks - - def assertXgesNumLayers(self, ges_el, compare): - """ - Assert that the ges element contains the expected number of - layers. - Returns the layers. - """ - layers = self.assertXgesNumElementsAtPath( - ges_el, "./project/timeline/layer", compare) - for layer in layers: - self.assertXgesHasAllAttrs(layer, "priority") - return layers - - def assertXgesLayer(self, ges_el, priority): - return self.assertXgesOneElementAtPathWithAttr( - ges_el, "./project/timeline/layer", - {"priority": str(priority)}) - - def assertXgesNumClipsAtPath(self, xml_el, path, compare): - """ - Assert that the xml element contains the expected number of - clips at the given path. - Returns the clips. - """ - clips = self.assertXgesNumElementsAtPath(xml_el, path, compare) - for clip in clips: - self.assertXgesHasAllAttrs( - clip, "id", "asset-id", "type-name", "layer-priority", - "track-types", "start", "duration", "inpoint", "rate", - "properties", "metadatas") - return clips - - def assertXgesNumClips(self, ges_el, compare): - """ - Assert that the ges element contains the expected number of - clips. - Returns the clips. - """ - return self.assertXgesNumClipsAtPath( - ges_el, "./project/timeline/layer/clip", compare) - - def assertXgesNumClipsInLayer(self, layer_el, compare): - """ - Assert that the layer element contains the expected number of - clips. - Returns the clips. - """ - return self.assertXgesNumClipsAtPath(layer_el, "./clip", compare) - - def assertXgesClip(self, ges_el, attrs): - """ - Assert that the ges element contains only one clip with the - given attributes. - Returns the matching clip. - """ - clip = self.assertXgesOneElementAtPathWithAttr( - ges_el, "./project/timeline/layer/clip", attrs) - self.assertXgesHasAllAttrs( - clip, "id", "asset-id", "type-name", "layer-priority", - "track-types", "start", "duration", "inpoint", "rate", - "properties", "metadatas") - return clip - - def assertXgesAsset(self, ges_el, asset_id, extract_type): - """ - Assert that the ges element contains only one asset with the - given id and extract type. - Returns the matching asset. - """ - asset = self.assertXgesOneElementAtPathWithAttr( - ges_el, "./project/ressources/asset", - {"id": asset_id, "extractable-type-name": extract_type}) - self.assertXgesHasAllAttrs( - asset, "id", "extractable-type-name", "properties", - "metadatas") - return asset - - def assertXgesClipHasAsset(self, ges_el, clip_el): - """ - Assert that the ges clip has a corresponding asset. - Returns the asset. - """ - asset_id = self.assertXgesHasAttr(clip_el, "asset-id") - extract_type = self.assertXgesHasAttr(clip_el, "type-name") - return self.assertXgesAsset(ges_el, asset_id, extract_type) - - def assertXgesClipIsSubproject(self, ges_el, clip_el): - """ - Assert that the ges clip corresponds to a subproject. - Retruns the subprojects ges element. - """ - self.assertXgesClipHasAsset(ges_el, clip_el) - ges_asset = self.assertXgesAsset( - ges_el, clip_el.get("asset-id"), "GESTimeline") - sub_ges_el = self.assertXgesOneElementAtPath(ges_asset, "ges") - self.assertXgesIsGesElement(sub_ges_el) - return sub_ges_el - - def assertXgesNumClipEffects(self, clip_el, compare): - """ - Assert that the clip element contains the expected number of - effects. - Returns the effects. - """ - effects = self.assertXgesNumElementsAtPath( - clip_el, "./effect", compare) - for effect in effects: - self.assertXgesHasAllAttrs( - effect, "asset-id", "clip-id", "type-name", - "track-type", "track-id", "properties", "metadatas", - "children-properties") - return effects - - def assertXgesTimelineMarkerListEqual(self, ges_el, marker_list): - timeline = self.assertXgesOneElementAtPath( - ges_el, "./project/timeline") - self.assertXgesMetadataEqual( - timeline, "markers", "GESMarkerList", marker_list) - - -class AdaptersXGESTest( - unittest.TestCase, otio_test_utils.OTIOAssertions, - CustomOtioAssertions, CustomXgesAssertions): - - def _get_xges_from_otio_timeline(self, timeline): - ges_el = ElementTree.fromstring( - otio.adapters.write_to_string(timeline, "xges")) - self.assertIsNotNone(ges_el) - self.assertXgesIsGesElement(ges_el) - return ges_el - - def test_read(self): - timeline = otio.adapters.read_from_file(XGES_EXAMPLE_PATH) - test_tree = OtioTestTree( - self, type_tests={ - Stack: [OtioTest.none_source], - Track: [OtioTest.none_source], - Clip: [OtioTest.has_ex_ref]}, - base=OtioTestNode(Stack, children=[ - OtioTestNode( - Track, tests=[OtioTest.is_audio], - children=[OtioTestNode(Clip)]), - OtioTestNode( - Track, tests=[OtioTest.is_video], - children=[ - OtioTestNode(Gap), OtioTestNode(Clip), - OtioTestNode(Transition), OtioTestNode(Clip) - ]), - OtioTestNode( - Track, tests=[OtioTest.is_video], - children=[ - OtioTestNode(Gap), OtioTestNode(Clip), - OtioTestNode(Gap), OtioTestNode(Clip) - ]), - OtioTestNode( - Track, tests=[OtioTest.is_audio], - children=[OtioTestNode(Gap), OtioTestNode(Clip)]), - OtioTestNode( - Track, tests=[OtioTest.is_video], - children=[OtioTestNode(Gap), OtioTestNode(Clip)]), - OtioTestNode( - Track, tests=[OtioTest.is_audio], - children=[OtioTestNode(Gap), OtioTestNode(Clip)]) - ])) - test_tree.test_compare(timeline.tracks) - - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesTrackTypes(ges_el, 2, 4) - self.assertXgesNumLayers(ges_el, 5) - ids = [] - for priority, expect_num, expect_track_types in zip( - range(5), [1, 1, 2, 3, 1], [6, 2, 4, 4, 2]): - layer = self.assertXgesLayer(ges_el, priority) - clips = self.assertXgesNumClipsInLayer(layer, expect_num) - for clip in clips: - ids.append(clip.get("id")) - self.assertXgesAttrEqual( - clip, "track-types", expect_track_types) - self.assertXgesAttrEqual( - clip, "layer-priority", priority) - if clip.get("type-name") == "GESUriClip": - self.assertXgesClipHasAsset(ges_el, clip) - # check that ids are unique - for clip_id in ids: - self.assertIsNotNone(clip_id) - self.assertEqual(ids.count(clip_id), 1) - - def test_unsupported_track_type(self): - # want to test that a project with an unsupported track type - # will still give results for the supported tracks - xges_el = XgesElement() - xges_el.add_audio_track() - # text is unsupported - xges_el.add_text_track() - xges_el.add_video_track() - xges_el.add_layer() - xges_el.add_clip(0, 2, 0, "GESUriClip", 14, name="mixed") - xges_el.add_clip(1, 1, 0, "GESTransitionClip", 6) - xges_el.add_clip(1, 2, 0, "GESUriClip", 6, name="audio-video") - xges_el.add_clip(3, 2, 0, "GESUriClip", 8, name="text") - - if str is not bytes: - # TODO: remove str is not bytes test when python2 ends - # Python2 does not have assertWarns - # warning because unsupported text track type - with self.assertWarns(UserWarning): - timeline = xges_el.get_otio_timeline() - else: - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, base=OtioTestNode(Stack, children=[ - OtioTestNode( - Track, tests=[OtioTest.is_video], children=[ - OtioTestNode(Clip), OtioTestNode(Transition), - OtioTestNode(Clip)]), - OtioTestNode( - Track, tests=[OtioTest.is_audio], children=[ - OtioTestNode(Clip), OtioTestNode(Transition), - OtioTestNode(Clip)]) - ])) - test_tree.test_compare(timeline.tracks) - - def test_project_name(self): - xges_el = XgesElement(UTF8_NAME) - timeline = xges_el.get_otio_timeline() - self.assertOtioAttrEqual(timeline, "name", UTF8_NAME) - ges_el = self._get_xges_from_otio_timeline(timeline) - project_el = ges_el.find("./project") - # already asserted that project_el exists with IsGesElement in - # _get_xges_from_otio_timeline - self.assertXgesMetadataEqual( - project_el, "name", "string", UTF8_NAME) - - def test_clip_names(self): - xges_el = XgesElement() - xges_el.add_audio_track() - xges_el.add_video_track() - xges_el.add_layer() - names = [UTF8_NAME, "T", "C"] - xges_el.add_clip(0, 2, 0, "GESUriClip", 6, name=names[0]) - xges_el.add_clip(1, 1, 0, "GESTransitionClip", 6, name=names[1]) - xges_el.add_clip(1, 2, 0, "GESUriClip", 6, name=names[2]) - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, base=OtioTestNode(Stack, children=[ - OtioTestNode(Track, children=[ - OtioTestNode( - Clip, tests=[OtioTest.name(names[0])]), - OtioTestNode( - Transition, tests=[OtioTest.name(names[1])]), - OtioTestNode( - Clip, tests=[OtioTest.name(names[2])]) - ]), - OtioTestNode(Track, children=[ - OtioTestNode( - Clip, tests=[OtioTest.name(names[0])]), - OtioTestNode( - Transition, tests=[OtioTest.name(names[1])]), - OtioTestNode( - Clip, tests=[OtioTest.name(names[2])]) - ]), - ])) - test_tree.test_compare(timeline.tracks) - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesNumClips(ges_el, 3) - for clip_id, name in zip(range(3), names): - clip = self.assertXgesClip(ges_el, {"id": clip_id}) - self.assertXgesPropertyEqual( - clip, "name", "string", name) - - def test_clip_names_unique(self): - xges_el = XgesElement() - xges_el.add_audio_track() - xges_el.add_layer() - xges_el.add_clip(0, 1, 0, "GESUriClip", 2, name="clip2") - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, base=OtioTestNode(Stack, children=[ - OtioTestNode(Track, children=[ - OtioTestNode( - Clip, tests=[OtioTest.name("clip2")]) - ]) - ])) - test_tree.test_compare(timeline.tracks) - timeline.tracks[0].append(_make_clip(name="clip2")) - timeline.tracks[0].append(_make_clip(name="clip2")) - ges_el = self._get_xges_from_otio_timeline(timeline) - clips = self.assertXgesNumClips(ges_el, 3) - clip_names = [] - for clip in clips: - name = self.assertXgesHasProperty(clip, "name", "string") - self.assertNotIn(name, clip_names) - clip_names.append(name) - - def test_asset(self): - xges_el = XgesElement() - xges_el.add_layer() - asset_id = "file:///ex%%mple" - duration = 235 - xges_el.add_asset(asset_id, "GESUriClip", duration) - xges_el.add_clip(0, 1, 5, "GESUriClip", 2, asset_id=asset_id) - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, base=OtioTestNode(Stack, children=[ - OtioTestNode(Track, children=[ - OtioTestNode( - Clip, tests=[OtioTest.has_ex_ref]) - ]) - ])) - test_tree.test_compare(timeline.tracks) - self.assertOtioAttrPathEqual( - timeline.tracks[0][0], ["media_reference", "target_url"], - asset_id) - self.assertOtioAttrPathEqual( - timeline.tracks[0][0], - ["media_reference", "available_range"], - _tm_range_from_secs(0, duration)) - ges_el = self._get_xges_from_otio_timeline(timeline) - asset = self.assertXgesAsset(ges_el, asset_id, "GESUriClip") - self.assertXgesPropertyEqual( - asset, "duration", "guint64", duration * GST_SECOND) - - def test_framerate(self): - xges_el = XgesElement() - framerate = 45.0 - xges_el.add_video_track(framerate) - xges_el.add_layer() - xges_el.add_clip(0, 1, 0, "GESUriClip", 4) - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, base=OtioTestNode(Stack, children=[ - OtioTestNode(Track, children=[ - OtioTestNode(Clip, tests=[ - OtioTest.range(0, 1), - OtioTest.rate(framerate)]) - ]) - ])) - test_tree.test_compare(timeline.tracks) - - def test_effects(self): - xges_el = XgesElement() - xges_el.add_audio_track() - xges_el.add_video_track() - xges_el.add_layer() - xges_el.add_clip(0, 1, 0, "GESUriClip", 6) - - video_effect_attribs = [{ - "asset-id": "agingtv", - "track-type": 4, - "track-id": 0, - "children-properties": SCHEMA.GstStructure.new_from_str( - "properties, GstAgingTV::color-aging=(boolean)true, " - "GstAgingTV::dusts=(boolean)true, " - "GstAgingTV::pits=(boolean)true, " - "GstBaseTransform::qos=(boolean)true, " - "GstAgingTV::scratch-lines=(uint)7;")}, { - "asset-id": "videobalance", - "track-type": 4, - "track-id": 0, - "children-properties": SCHEMA.GstStructure.new_from_str( - "properties, GstVideoBalance::brightness=(double)0, " - "GstVideoBalance::contrast=(double)0.5, " - "GstVideoBalance::hue=(double)0, " - "GstBaseTransform::qos=(boolean)true, " - "GstVideoBalance::saturation=(double)1;")}] - audio_effect_attribs = [{ - "asset-id": "audiokaraoke", - "track-type": 2, - "track-id": 1, - "children-properties": SCHEMA.GstStructure.new_from_str( - "properties, GstAudioKaraoke::filter-band=(float)220, " - "GstAudioKaraoke::filter-width=(float)100, " - "GstAudioKaraoke::level=(float)1, " - "GstAudioKaraoke::mono-level=(float)1, " - "GstBaseTransform::qos=(boolean)false;")}] - effect_attribs = [ - video_effect_attribs[0], audio_effect_attribs[0], - video_effect_attribs[1]] - for attrs in effect_attribs: - xges_el.add_effect( - attrs["asset-id"], attrs["track-type"], - attrs["track-id"], - children_properties=attrs["children-properties"]) - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, type_tests={ - Stack: [OtioTest.no_effects], - Track: [OtioTest.no_effects]}, - base=OtioTestNode(Stack, children=[ - OtioTestNode( - Track, tests=[OtioTest.is_video], children=[ - OtioTestNode( - Clip, tests=[OtioTest.effects( - "agingtv", "videobalance")]) - ]), - OtioTestNode( - Track, tests=[OtioTest.is_audio], children=[ - OtioTestNode( - Clip, tests=[OtioTest.effects( - "audiokaraoke")]) - ]) - ])) - test_tree.test_compare(timeline.tracks) - ges_el = self._get_xges_from_otio_timeline(timeline) - tracks = self.assertXgesTrackTypes(ges_el, 2, 4) - audio_track = tracks[0] - video_track = tracks[1] - layers = self.assertXgesNumLayers(ges_el, 2) - # expect 2 layers since re-merging of the tracks will be - # prevented by the different effects for different track types - clip = self.assertXgesNumClipsInLayer(layers[0], 1)[0] - audio_effects = self.assertXgesNumClipEffects( - clip, len(audio_effect_attribs)) - for effect, attrs in zip(audio_effects, audio_effect_attribs): - self.assertXgesAttrEqual( - effect, "asset-id", attrs["asset-id"]) - self.assertXgesAttrEqual(effect, "track-type", 2) - self.assertXgesAttrEqual( - effect, "track-id", audio_track.get("track-id")) - self.assertXgesStructureEqual( - effect, "children-properties", - attrs["children-properties"]) - clip = self.assertXgesNumClipsInLayer(layers[1], 1)[0] - video_effects = self.assertXgesNumClipEffects( - clip, len(video_effect_attribs)) - for effect, attrs in zip(video_effects, video_effect_attribs): - self.assertXgesAttrEqual( - effect, "asset-id", attrs["asset-id"]) - self.assertXgesAttrEqual(effect, "track-type", 4) - self.assertXgesAttrEqual( - effect, "track-id", video_track.get("track-id")) - self.assertXgesStructureEqual( - effect, "children-properties", - attrs["children-properties"]) - - def test_track_effects(self): - timeline = Timeline() - effect_names = ["agingtv", "videobalance"] - track = Track() - track.kind = TrackKind.Video - timeline.tracks.append(track) - for name in effect_names: - track.effects.append(Effect(effect_name=name)) - track.append(Gap(source_range=_tm_range_from_secs(0, 3))) - track.append(_make_clip(start=2, duration=5)) - track.append(_make_clip(start=0, duration=4)) - - if str is not bytes: - # TODO: remove str is not bytes test when python2 ends - # Python2 does not have assertWarns - # TODO: warning is for the fact that we do not yet have a - # smart way to convert effect names into bin-descriptions - # Should be removed once this is sorted - with self.assertWarns(UserWarning): - ges_el = self._get_xges_from_otio_timeline(timeline) - else: - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesTrackTypes(ges_el, 4) - layer = self.assertXgesNumLayers(ges_el, 1)[0] - self.assertXgesNumClipsInLayer(layer, 4) - ids = [] - ids.append(self.assertXgesClip( - ges_el, { - "start": 3, "duration": 5, "inpoint": 2, - "type-name": "GESUriClip", "track-types": 4}).get("id")) - ids.append(self.assertXgesClip( - ges_el, { - "start": 8, "duration": 4, "inpoint": 0, - "type-name": "GESUriClip", "track-types": 4}).get("id")) - ids.append(self.assertXgesClip( - ges_el, { - "start": 3, "duration": 9, "inpoint": 0, - "asset-id": effect_names[0], - "type-name": "GESEffectClip", "track-types": 4}).get("id")) - ids.append(self.assertXgesClip( - ges_el, { - "start": 3, "duration": 9, "inpoint": 0, - "asset-id": effect_names[1], - "type-name": "GESEffectClip", "track-types": 4}).get("id")) - # check that ids are unique - for clip_id in ids: - self.assertIsNotNone(clip_id) - self.assertEqual(ids.count(clip_id), 1) - - def test_markers(self): - marker_list = SCHEMA.GESMarkerList( - _make_ges_marker(23, MarkerColor.RED), - _make_ges_marker(30), - _make_ges_marker( - 77, MarkerColor.BLUE, UTF8_NAME, SCHEMA.GstStructure( - "metadatas", {"Int": ("int", 30)}))) - # Note, the second marker is not colored, so we don't expect a - # corresponding otio marker - marker_list[2].set_color_from_otio_color(MarkerColor.BLUE) - xges_el = XgesElement(marker_list=marker_list) - xges_el.add_audio_track() - xges_el.add_layer() - xges_el.add_clip(1, 1, 0, "GESUriClip", 2) - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, type_tests={ - Track: [OtioTest.no_markers], - Clip: [OtioTest.no_markers], - Gap: [OtioTest.no_markers]}, - base=OtioTestNode( - Stack, tests=[OtioTest.markers({ - "name": "", "color": MarkerColor.RED, - "start": 23, "duration": 0}, { - "name": UTF8_NAME, "color": MarkerColor.BLUE, - "start": 77, "duration": 0})], - children=[ - OtioTestNode(Track, children=[ - OtioTestNode(Gap), - OtioTestNode(Clip) - ]) - ])) - test_tree.test_compare(timeline.tracks) - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesTrackTypes(ges_el, 2) - layer = self.assertXgesNumLayers(ges_el, 1)[0] - self.assertXgesNumClipsInLayer(layer, 1)[0] - self.assertXgesTimelineMarkerListEqual(ges_el, marker_list) - - def _add_test_properties_and_metadatas(self, el): - el.attrib["properties"] = str(SCHEMA.GstStructure( - "properties", { - "field2": ("int", 5), - "field1": ("string", UTF8_NAME)})) - el.attrib["metadatas"] = str(SCHEMA.GstStructure( - "metadatas", { - "field3": ("int", 6), - "field4": ("boolean", True)})) - - def _has_test_properties_and_metadatas(self, el): - self.assertXgesPropertyEqual(el, "field1", "string", UTF8_NAME) - self.assertXgesPropertyEqual(el, "field2", "int", 5) - self.assertXgesMetadataEqual(el, "field3", "int", 6) - self.assertXgesMetadataEqual(el, "field4", "boolean", True) - - def test_clip_properties_and_metadatas(self): - xges_el = XgesElement() - xges_el.add_video_track() - xges_el.add_layer() - clip = xges_el.add_clip(0, 1, 0, "GESUriClip", 4) - self._add_test_properties_and_metadatas(clip) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas( - self.assertXgesClip(ges_el, {})) - - def test_transition_properties_and_metadatas(self): - xges_el = XgesElement() - xges_el.add_video_track() - xges_el.add_layer() - xges_el.add_clip(0, 2, 0, "GESUriClip", 4) - transition = xges_el.add_clip(1, 1, 0, "GESTransitionClip", 4) - self._add_test_properties_and_metadatas(transition) - xges_el.add_clip(1, 2, 0, "GESUriClip", 4) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas(self.assertXgesClip( - ges_el, {"type-name": "GESTransitionClip"})) - - def test_project_properties_and_metadatas(self): - xges_el = XgesElement() - self._add_test_properties_and_metadatas(xges_el.project) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas( - self.assertXgesOneElementAtPath(ges_el, "./project")) - - def test_timeline_properties_and_metadatas(self): - xges_el = XgesElement() - self._add_test_properties_and_metadatas(xges_el.timeline) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas( - self.assertXgesOneElementAtPath( - ges_el, "./project/timeline")) - - def test_layer_properties_and_metadatas(self): - xges_el = XgesElement() - xges_el.add_video_track() - layer = xges_el.add_layer() - self._add_test_properties_and_metadatas(layer) - # NOTE: need a non-empty layer - xges_el.add_clip(0, 2, 0, "GESUriClip", 4) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas( - self.assertXgesNumLayers(ges_el, 1)[0]) - - def test_uri_clip_asset_properties_and_metadatas(self): - xges_el = XgesElement() - xges_el.add_video_track() - xges_el.add_layer() - asset_id = "file:///example-file" - asset = xges_el.add_asset(asset_id, "GESUriClip") - self._add_test_properties_and_metadatas(asset) - xges_el.add_clip(0, 1, 0, "GESUriClip", 4, asset_id) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas( - self.assertXgesAsset(ges_el, asset_id, "GESUriClip")) - - def _subproject_asset_props_and_metas_for_type(self, extract_type): - xges_el = self._make_nested_project() - asset = xges_el.ressources.find( - f"./asset[@extractable-type-name='{extract_type}']") - self.assertIsNotNone(asset) - asset_id = asset.get("id") - self.assertIsNotNone(asset_id) - self._add_test_properties_and_metadatas(asset) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas( - self.assertXgesAsset(ges_el, asset_id, extract_type)) - - def test_subproject_asset_properties_and_metadatas(self): - self._subproject_asset_props_and_metas_for_type("GESUriClip") - self._subproject_asset_props_and_metas_for_type("GESTimeline") - - def test_track_properties_and_metadatas(self): - xges_el = XgesElement() - track = xges_el.add_audio_track() - self._add_test_properties_and_metadatas(track) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self._has_test_properties_and_metadatas( - self.assertXgesOneElementAtPath( - ges_el, "./project/timeline/track")) - - def test_effect_properties_and_metadatas(self): - xges_el = XgesElement() - xges_el.add_video_track() - xges_el.add_layer() - xges_el.add_clip(0, 1, 0, "GESUriClip", 4) - effect = xges_el.add_effect("videobalance", 4, 0) - self._add_test_properties_and_metadatas(effect) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - clip = self.assertXgesClip(ges_el, {}) - self._has_test_properties_and_metadatas( - self.assertXgesNumClipEffects(clip, 1)[0]) - - def test_empty_timeline(self): - xges_el = XgesElement() - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, base=OtioTestNode( - Stack, tests=[OtioTest.none_source])) - test_tree.test_compare(timeline.tracks) - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesNumLayers(ges_el, 0) - - def SKIP_test_empty_layer(self): - # Test will fail since empty layers are lost! - xges_el = XgesElement() - xges_el.add_layer() - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, base=OtioTestNode( - Stack, tests=[OtioTest.none_source], children=[ - OtioTestNode(Track, tests=[OtioTest.none_source])])) - test_tree.test_compare(timeline.tracks) - ges_el = self._get_xges_from_otio_timeline(timeline) - layer_el = self.assertXgesNumLayers(ges_el, 1)[0] - self.assertXgesNumClipsInLayer(layer_el, 0) - - def test_timing(self): - # example input layer is of the form: - # [------] - # [---------------] - # [-----------] [--][--] - # - # 0 1 2 3 4 5 6 7 8 9 10 11 - # time in seconds - # - # where [----] are clips. The first clip has an inpoint of - # 15 seconds, and the second has an inpoint of 25 seconds. The - # rest have an inpoint of 0 - xges_el = XgesElement() - xges_el.add_audio_track() - xges_el.add_layer() - xges_el.add_clip(1, 2, 15, "GESUriClip", 2) - xges_el.add_clip(2, 1, 0, "GESTransitionClip", 2) - xges_el.add_clip(2, 4, 25, "GESUriClip", 2) - xges_el.add_clip(4, 2, 0, "GESTransitionClip", 2) - xges_el.add_clip(4, 3, 0, "GESUriClip", 2) - xges_el.add_clip(9, 1, 0, "GESUriClip", 2) - xges_el.add_clip(10, 1, 0, "GESUriClip", 2) - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, type_tests={ - Stack: [OtioTest.none_source], - Track: [ - OtioTest.none_source, OtioTest.is_audio], - Clip: [OtioTest.has_ex_ref]}, - base=OtioTestNode(Stack, children=[ - OtioTestNode(Track, children=[ - OtioTestNode(Gap, tests=[ - OtioTest.range_in_parent(0, 1)]), - OtioTestNode(Clip, tests=[ - OtioTest.range_in_parent(1, 1.5), - OtioTest.start_time(15)]), - OtioTestNode(Transition, tests=[ - OtioTest.offset_total(1)]), - OtioTestNode(Clip, tests=[ - OtioTest.range_in_parent(2.5, 2.5), - OtioTest.start_time(25.5)]), - OtioTestNode(Transition, tests=[ - OtioTest.offset_total(2)]), - OtioTestNode(Clip, tests=[ - OtioTest.range_in_parent(5, 2)]), - OtioTestNode(Gap, tests=[ - OtioTest.range_in_parent(7, 2)]), - OtioTestNode(Clip, tests=[ - OtioTest.range_in_parent(9, 1)]), - OtioTestNode(Clip, tests=[ - OtioTest.range_in_parent(10, 1)]) - ]) - ])) - test_tree.test_compare(timeline.tracks) - - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesTrackTypes(ges_el, 2) - self.assertXgesNumClips(ges_el, 7) - self.assertXgesClip( - ges_el, { - "start": 1, "duration": 2, "inpoint": 15, - "type-name": "GESUriClip", "track-types": 2}) - self.assertXgesClip( - ges_el, { - "start": 2, "duration": 1, "inpoint": 0, - "type-name": "GESTransitionClip", "track-types": 2}) - self.assertXgesClip( - ges_el, { - "start": 2, "duration": 4, "inpoint": 25, - "type-name": "GESUriClip", "track-types": 2}) - self.assertXgesClip( - ges_el, { - "start": 4, "duration": 2, "inpoint": 0, - "type-name": "GESTransitionClip", "track-types": 2}) - self.assertXgesClip( - ges_el, { - "start": 4, "duration": 3, "inpoint": 0, - "type-name": "GESUriClip", "track-types": 2}) - self.assertXgesClip( - ges_el, { - "start": 9, "duration": 1, "inpoint": 0, - "type-name": "GESUriClip", "track-types": 2}) - self.assertXgesClip( - ges_el, { - "start": 10, "duration": 1, "inpoint": 0, - "type-name": "GESUriClip", "track-types": 2}) - - def _make_nested_project(self): - xges_el = XgesElement() - xges_el.add_video_track() - xges_el.add_audio_track() - xges_el.add_layer() - asset = xges_el.add_asset("file:///example.xges", "GESTimeline") - xges_el.add_clip( - 70, 20, 10, "GESUriClip", 6, "file:///example.xges") - sub_xges_el = XgesElement() - sub_xges_el.add_video_track() - sub_xges_el.add_layer() - sub_xges_el.add_clip(50, 40, 30, "GESUriClip", 6) - asset.append(sub_xges_el.ges) - return xges_el - - def test_nested_projects_and_stacks(self): - xges_el = self._make_nested_project() - timeline = xges_el.get_otio_timeline() - test_tree = OtioTestTree( - self, type_tests={ - Track: [OtioTest.none_source], - Clip: [OtioTest.has_ex_ref]}, - base=OtioTestNode( - Stack, tests=[OtioTest.none_source], children=[ - OtioTestNode( - Track, tests=[OtioTest.is_video], - children=[ - OtioTestNode( - Gap, - tests=[OtioTest.duration(70)]), - OtioTestNode( - Stack, - tests=[OtioTest.range(10, 20)], - children=[ - OtioTestNode( - Track, - tests=[OtioTest.is_video], - children=[ - OtioTestNode(Gap, tests=[ - OtioTest.duration(50)]), - OtioTestNode(Clip, tests=[ - OtioTest.range(30, 40)]) - ]), - OtioTestNode( - Track, - tests=[OtioTest.is_audio], - children=[ - OtioTestNode(Gap, tests=[ - OtioTest.duration(50)]), - OtioTestNode(Clip, tests=[ - OtioTest.range(30, 40)]) - ]) - ]) - ]), - OtioTestNode( - Track, tests=[OtioTest.is_audio], - children=[ - OtioTestNode( - Gap, - tests=[OtioTest.duration(70)]), - OtioTestNode( - Stack, - tests=[OtioTest.range(10, 20)], - children=[ - OtioTestNode( - Track, - tests=[OtioTest.is_video], - children=[ - OtioTestNode(Gap, tests=[ - OtioTest.duration(50)]), - OtioTestNode(Clip, tests=[ - OtioTest.range(30, 40)]) - ]), - OtioTestNode( - Track, - tests=[OtioTest.is_audio], - children=[ - OtioTestNode(Gap, tests=[ - OtioTest.duration(50)]), - OtioTestNode(Clip, tests=[ - OtioTest.range(30, 40)]) - ]) - ]) - ]) - ])) - test_tree.test_compare(timeline.tracks) - self._xges_has_nested_clip(timeline, 70, 20, 10, 6, 50, 40, 30, 6) - - def test_nested_projects_and_stacks_edited(self): - xges_el = self._make_nested_project() - timeline = xges_el.get_otio_timeline() - # Previous test will assert the correct structure - - # Change the gap before the video sub-stack to 30 seconds - timeline.tracks[0][0].source_range = _tm_range_from_secs(0, 30) - - # The sub-project should be the same, but we now have two - # different clips referencing the same sub-project - - # Now have an audio clip, with the new start time - first_top_clip, _ = self._xges_has_nested_clip( - timeline, 30, 20, 10, 4, 50, 40, 30, 6) - # And the video clip, with the old start time - second_top_clip, _ = self._xges_has_nested_clip( - timeline, 70, 20, 10, 2, 50, 40, 30, 6) - # They both reference the same project - first_id = self.assertXgesHasAttr(first_top_clip, "asset-id") - self.assertXgesAttrEqual(second_top_clip, "asset-id", first_id) - - # Restore the timing - timeline.tracks[0][0].source_range = _tm_range_from_secs(0, 70) - # Change the video sub-stack to reference an earlier point - timeline.tracks[0][1].source_range = _tm_range_from_secs(0, 10) - - # The sub-project should be the same, but we now have two - # different clips referencing the same sub-project - - # Now have a video clip, with the new duration and inpoint - first_top_clip, _ = self._xges_has_nested_clip( - timeline, 70, 10, 0, 4, 50, 40, 30, 6) - # And an audio clip, with the old start time - second_top_clip, _ = self._xges_has_nested_clip( - timeline, 70, 20, 10, 2, 50, 40, 30, 6) - # They both reference the same project - first_id = self.assertXgesHasAttr(first_top_clip, "asset-id") - self.assertXgesAttrEqual(second_top_clip, "asset-id", first_id) - - # Restore the timing - timeline.tracks[0][1].source_range = _tm_range_from_secs(10, 20) - # Change the content of the video sub-stack by reducing the gap - timeline.tracks[0][1][0][0].source_range = _tm_range_from_secs(0, 20) - timeline.tracks[0][1][1][0].source_range = _tm_range_from_secs(0, 20) - - # The sub-project should now be different, so we should have two - # separate assets - first_top_clip, _ = self._xges_has_nested_clip( - timeline, 70, 20, 10, 4, 20, 40, 30, 6) - second_top_clip, _ = self._xges_has_nested_clip( - timeline, 70, 20, 10, 2, 50, 40, 30, 6) - # They now reference different projects - first_id = self.assertXgesHasAttr(first_top_clip, "asset-id") - second_id = self.assertXgesHasAttr(second_top_clip, "asset-id") - self.assertNotEqual(first_id, second_id) - - # Restore the stack's timing - timeline.tracks[0][1][0][0].source_range = _tm_range_from_secs(0, 50) - timeline.tracks[0][1][1][0].source_range = _tm_range_from_secs(0, 50) - # Change the content of the video sub-stack by referencing - # different times for its clip - timeline.tracks[0][1][0][1].source_range = _tm_range_from_secs(10, 60) - timeline.tracks[0][1][1][1].source_range = _tm_range_from_secs(10, 60) - - # The sub-project should now be different, so we should have two - # separate assets - first_top_clip, _ = self._xges_has_nested_clip( - timeline, 70, 20, 10, 4, 50, 60, 10, 6) - second_top_clip, _ = self._xges_has_nested_clip( - timeline, 70, 20, 10, 2, 50, 40, 30, 6) - # They now reference different projects - first_id = self.assertXgesHasAttr(first_top_clip, "asset-id") - second_id = self.assertXgesHasAttr(second_top_clip, "asset-id") - self.assertNotEqual(first_id, second_id) - - def _xges_has_nested_clip( - self, timeline, - top_start, top_duration, top_inpoint, top_track_types, - orig_start, orig_duration, orig_inpoint, orig_track_types, - effect_names=None): - """Returns the top clip and nested clip""" - if effect_names is None: - effect_names = [] - - if effect_names and str is not bytes: - # TODO: remove the str is not bytes check once python2 has - # ended. Python2 does not have assertWarns - # TODO: warning is for the fact that we do not yet have a - # smart way to convert effect names into bin-descriptions - # Should be removed once this is sorted - with self.assertWarns(UserWarning): - ges_el = self._get_xges_from_otio_timeline(timeline) - else: - ges_el = self._get_xges_from_otio_timeline(timeline) - if orig_track_types == 6: - self.assertXgesTrackTypes(ges_el, 2, 4) - else: - self.assertXgesTrackTypes(ges_el, top_track_types) - top_clip = self.assertXgesClip( - ges_el, { - "start": top_start, "duration": top_duration, - "inpoint": top_inpoint, "type-name": "GESUriClip", - "track-types": top_track_types}) - effects = self.assertXgesNumClipEffects( - top_clip, len(effect_names)) - for effect, name in zip(effects, effect_names): - self.assertXgesAttrEqual(effect, "asset-id", name) - - ges_el = self.assertXgesClipIsSubproject(ges_el, top_clip) - self.assertXgesNumClips(ges_el, 1) - orig_clip = self.assertXgesClip( - ges_el, { - "start": orig_start, "duration": orig_duration, - "inpoint": orig_inpoint, "type-name": "GESUriClip", - "track-types": orig_track_types}) - self.assertXgesNumClipEffects(orig_clip, 0) - self.assertXgesClipHasAsset(ges_el, orig_clip) - return top_clip, orig_clip - - def test_effect_stack(self): - timeline = Timeline() - effect_names = ["agingtv", "videobalance"] - for name in effect_names: - timeline.tracks.effects.append(Effect(effect_name=name)) - track = Track() - track.kind = TrackKind.Video - timeline.tracks.append(track) - track.append(_make_clip(start=20, duration=50)) - self._xges_has_nested_clip( - timeline, 0, 50, 0, 4, 0, 50, 20, 4, effect_names) - - def test_source_range_stack(self): - timeline = Timeline() - track = Track() - track.kind = TrackKind.Video - timeline.tracks.append(track) - track.append(_make_clip(start=20, duration=50)) - timeline.tracks.source_range = _tm_range_from_secs(10, 30) - self._xges_has_nested_clip(timeline, 0, 30, 10, 4, 0, 50, 20, 4) - - def test_source_range_track(self): - timeline = Timeline() - track = Track() - track.kind = TrackKind.Video - timeline.tracks.append(track) - track.append(_make_clip(start=20, duration=50)) - track.source_range = _tm_range_from_secs(10, 30) - self._xges_has_nested_clip(timeline, 0, 30, 10, 4, 0, 50, 20, 4) - - def test_double_track(self): - timeline = Timeline() - track1 = Track() - track1.kind = TrackKind.Video - timeline.tracks.append(track1) - track2 = Track() - track2.kind = TrackKind.Video - track1.append(_make_clip(start=40, duration=90)) - track1.append(track2) - track2.append(_make_clip(start=20, duration=50)) - self._xges_has_nested_clip(timeline, 90, 50, 0, 4, 0, 50, 20, 4) - - def test_double_stack(self): - timeline = Timeline() - stack = Stack() - stack.source_range = _tm_range_from_secs(10, 30) - track = Track() - track.kind = TrackKind.Video - track.append(_make_clip(start=20, duration=50)) - stack.append(track) - track = Track() - track.kind = TrackKind.Video - track.append(_make_clip()) - timeline.tracks.append(track) - timeline.tracks.append(stack) - self._xges_has_nested_clip(timeline, 0, 30, 10, 4, 0, 50, 20, 4) - - def test_track_merge(self): - timeline = Timeline() - for kind in [ - TrackKind.Audio, - TrackKind.Video]: - track = Track() - track.kind = kind - track.metadata["example-non-xges"] = str(kind) - track.metadata["XGES"] = { - "data": SCHEMA.GstStructure.new_from_str( - "name, key1=(string)hello, key2=(int)9;")} - track.append(_make_clip(start=2, duration=5)) - timeline.tracks.append(track) - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesClip( - ges_el, { - "start": 0, "duration": 5, "inpoint": 2, - "type-name": "GESUriClip", "track-types": 6}) - - # make tracks have different XGES metadata - for track in timeline.tracks: - track.metadata["XGES"]["data"].set( - "key1", "string", str(track.kind)) - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesClip( - ges_el, { - "start": 0, "duration": 5, "inpoint": 2, - "type-name": "GESUriClip", "track-types": 2}) - self.assertXgesClip( - ges_el, { - "start": 0, "duration": 5, "inpoint": 2, - "type-name": "GESUriClip", "track-types": 4}) - - def test_markers_from_otio(self): - timeline = Timeline() - _add_marker(timeline.tracks, "top marker", MarkerColor.PINK, 1, 0) - _add_marker(timeline.tracks, "", MarkerColor.ORANGE, 5, 3) - # duplicates are to be ignored - _add_marker(timeline.tracks, "top marker", MarkerColor.PINK, 1, 0) - _add_marker(timeline.tracks, "", MarkerColor.ORANGE, 5, 3) - track = Track() - timeline.tracks.append(track) - _add_marker(track, "track marker", MarkerColor.PURPLE, 2, 2) - _add_marker(track, "", MarkerColor.BLACK, 2, 0) - clip = _make_clip(duration=4) - track.append(clip) - _add_marker(clip, "clip1", MarkerColor.YELLOW, 1, 0) - gap = Gap(source_range=_tm_range_from_secs(0, 2)) - track.append(gap) - _add_marker(gap, "gap", MarkerColor.WHITE, 1, 0) - clip = _make_clip(duration=5) - track.append(clip) - _add_marker(clip, "clip2", MarkerColor.ORANGE, 2, 0) - _add_marker(clip, "", MarkerColor.GREEN, 1, 2) - - stack = Stack() - track.append(stack) - _add_marker(stack, "sub-stack", MarkerColor.RED, 1, 0) - track = Track() - stack.append(track) - _add_marker(track, "sub-track", MarkerColor.BLUE, 2, 0) - track.append(_make_clip(duration=3)) - clip = _make_clip(duration=2) - track.append(clip) - _add_marker(clip, "sub-clip", MarkerColor.MAGENTA, 1, 1) - - ges_el = self._get_xges_from_otio_timeline(timeline) - layer = self.assertXgesNumLayers(ges_el, 1)[0] - clips = self.assertXgesNumClipsInLayer(layer, 3) - self.assertXgesTimelineMarkerListEqual( - ges_el, SCHEMA.GESMarkerList( - _make_ges_marker(1, MarkerColor.PINK, "top marker"), - _make_ges_marker(5, MarkerColor.ORANGE), - _make_ges_marker(8, MarkerColor.ORANGE), - # 8 is the end of the marker range - _make_ges_marker(2, MarkerColor.PURPLE, "track marker"), - _make_ges_marker(4, MarkerColor.PURPLE, "track marker"), - _make_ges_marker(2, MarkerColor.BLACK), - _make_ges_marker(1, MarkerColor.YELLOW, "clip1"), - _make_ges_marker(5, MarkerColor.WHITE, "gap"), - # 5 = 4 + 1, since we want the position relative to the - # timeline, rather than the gap - _make_ges_marker(8, MarkerColor.ORANGE, "clip2"), - # Note, this matches the color and position of another - # marker, but we want both since this has a different - # comment - _make_ges_marker(7, MarkerColor.GREEN), - _make_ges_marker(9, MarkerColor.GREEN))) - - sub_ges_el = self.assertXgesClipIsSubproject(ges_el, clips[2]) - layer = self.assertXgesNumLayers(sub_ges_el, 1)[0] - clips = self.assertXgesNumClipsInLayer(layer, 2) - self.assertXgesTimelineMarkerListEqual( - sub_ges_el, SCHEMA.GESMarkerList( - _make_ges_marker(1, MarkerColor.RED, "sub-stack"), - _make_ges_marker(2, MarkerColor.BLUE, "sub-track"), - _make_ges_marker(4, MarkerColor.MAGENTA, "sub-clip"), - _make_ges_marker(5, MarkerColor.MAGENTA, "sub-clip"))) - - def test_timeline_is_unchanged(self): - timeline = Timeline(name="example") - timeline.tracks.source_range = _tm_range_from_secs(4, 5) - track = Track("Track", source_range=_tm_range_from_secs(2, 3)) - track.metadata["key"] = 5 - track.append(_make_clip()) - timeline.tracks.append(track) - - before = timeline.deepcopy() - otio.adapters.write_to_string(timeline, "xges") - self.assertIsOTIOEquivalentTo(before, timeline) - - def test_XgesTrack_usage(self): - xges_el = XgesElement() - xges_el.add_layer() - xges_el.add_clip(0, 1, 0, "GESUriClip", 4) - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesTrackTypes(ges_el) # assert no tracks! - - props_before = xges_el.add_video_track().get("properties") - timeline = xges_el.get_otio_timeline() - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertXgesTrackTypes(ges_el, 4) - track = self.assertXgesOneElementAtPath( - ges_el, "./project/timeline/track") - self.assertXgesStructureEqual(track, "properties", props_before) - - def test_XgesTrack_from_kind(self): - vid = SCHEMA.XgesTrack.\ - new_from_otio_track_kind(TrackKind.Video) - self.assertEqual(vid.track_type, 4) - aud = SCHEMA.XgesTrack.\ - new_from_otio_track_kind(TrackKind.Audio) - self.assertEqual(aud.track_type, 2) - - def test_XgesTrack_equality(self): - vid1 = SCHEMA.XgesTrack.\ - new_from_otio_track_kind(TrackKind.Video) - vid2 = SCHEMA.XgesTrack.\ - new_from_otio_track_kind(TrackKind.Video) - aud = SCHEMA.XgesTrack.\ - new_from_otio_track_kind(TrackKind.Audio) - self.assertTrue(vid1.is_equivalent_to(vid2)) - self.assertFalse(vid1.is_equivalent_to(aud)) - - def test_GstCaps_parsing(self): - caps = SCHEMA.GstCaps.new_from_str("ANY") - self.assertTrue(caps.is_any()) - self.assertEqual(len(caps), 0) - caps = SCHEMA.GstCaps.new_from_str( - "First( memory:SystemMemory, other:az09AZ) , " - "field1 = ( int ) 5 ,field2=(string){};" - "Second, fieldA=(fraction)3/67, fieldB=(boolean)true; " - "Third(ANY), fieldX=(int)-2".format( - SCHEMA.GstStructure.serialize_string(UTF8_NAME))) - self.assertFalse(caps.is_any()) - self.assertEqual(len(caps), 3) - struct = caps[0] - features = caps.get_features(0) - self.assertEqual(features.is_any, False) - self.assertEqual(len(features), 2) - self.assertEqual(features[0], "memory:SystemMemory") - self.assertEqual(features[1], "other:az09AZ") - self.assertEqual(struct.name, "First") - self.assertEqual(struct["field1"], 5) - self.assertEqual(struct["field2"], UTF8_NAME) - struct = caps[1] - features = caps.get_features(1) - self.assertEqual(features.is_any, False) - self.assertEqual(len(features), 0) - self.assertEqual(struct.name, "Second") - self.assertEqual(struct["fieldA"], "3/67") - self.assertEqual(struct["fieldB"], True) - struct = caps[2] - features = caps.get_features(2) - self.assertEqual(features.is_any, True) - self.assertEqual(len(features), 0) - self.assertEqual(struct.name, "Third") - self.assertEqual(struct["fieldX"], -2) - - def test_GstCaps_to_str(self): - caps_list = [ - {"caps": SCHEMA.GstCaps.new_any(), "str": "ANY"}, - { - "caps": SCHEMA.GstCaps( - SCHEMA.GstStructure("video/x-raw"), - SCHEMA.GstCapsFeatures.new_any()), - "str": "video/x-raw(ANY)"}, - { - "caps": SCHEMA.GstCaps( - SCHEMA.GstStructure( - "First", {"field1": ("string", UTF8_NAME)}), - SCHEMA.GstCapsFeatures( - "memory:SystemMemory", "other:az09AZ"), - SCHEMA.GstStructure( - "Second", {"fieldA": ("boolean", True)}), - None, - SCHEMA.GstStructure("Third", {"fieldX": ("int", -2)}), - SCHEMA.GstCapsFeatures.new_any()), - "str": - "First(memory:SystemMemory, other:az09AZ), " - "field1=(string){}; " - "Second, fieldA=(boolean)true; " - "Third(ANY), fieldX=(int)-2".format( - SCHEMA.GstStructure.serialize_string(UTF8_NAME))}] - for caps in caps_list: - string = str(caps["caps"]) - self.assertEqual(string, caps["str"]) - self.assertTrue(caps["caps"].is_equivalent_to( - SCHEMA.GstCaps.new_from_str(string))) - - def test_empty_GstCaps(self): - caps = SCHEMA.GstCaps() - self.assertEqual(len(caps), 0) - self.assertFalse(caps.is_any()) - self.assertEqual(str(caps), "EMPTY") - caps = SCHEMA.GstCaps.new_from_str("") - self.assertEqual(len(caps), 0) - self.assertFalse(caps.is_any()) - caps = SCHEMA.GstCaps.new_from_str("EMPTY") - self.assertEqual(len(caps), 0) - - def test_GstCapsFeatures_parsing(self): - features = SCHEMA.GstCapsFeatures.new_from_str("ANY") - self.assertEqual(features.is_any, True) - self.assertEqual(len(features), 0) - features = SCHEMA.GstCapsFeatures.new_from_str( - " memory:SystemMemory, other:az09AZ") - self.assertEqual(features.is_any, False) - self.assertEqual(len(features), 2) - self.assertEqual(features[0], "memory:SystemMemory") - self.assertEqual(features[1], "other:az09AZ") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstCapsFeatures.new_from_str("ANY ") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstCapsFeatures.new_from_str("memory") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstCapsFeatures.new_from_str("memory:") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstCapsFeatures.new_from_str("memory:0") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstCapsFeatures.new_from_str("mem0:a") - - def test_GESMarker_colors(self): - marker = SCHEMA.GESMarker(20) - self.assertEqual(marker.position, 20) - self.assertFalse(marker.is_colored()) - argb = 0x850fe409 - marker.set_color_from_argb(argb) - self.assertTrue(marker.is_colored()) - self.assertEqual(marker.get_argb_color(), argb) - marker = SCHEMA.GESMarker(20) - with self.assertRaises(otio.exceptions.OTIOError): - marker.set_color_from_argb(-1) - with self.assertRaises(otio.exceptions.OTIOError): - # too big - marker.set_color_from_argb(0xffffffff + 1) - - def test_GESMarker_color_to_otio_color(self): - marker = SCHEMA.GESMarker(20) - for otio_color in [col for col in dir(MarkerColor) - if col.isupper()]: - # should catch if otio adds a new color - marker.set_color_from_otio_color(otio_color) - self.assertTrue(marker.is_colored()) - nearest_otio = marker.get_nearest_otio_color() - self.assertEqual(otio_color, nearest_otio) - - def test_GESMarkerList_ordering(self): - marker_list = SCHEMA.GESMarkerList() - marker_list.add(SCHEMA.GESMarker(224)) - marker_list.add(SCHEMA.GESMarker(226)) - marker_list.add(SCHEMA.GESMarker(223)) - marker_list.add(SCHEMA.GESMarker(224)) - marker_list.add(SCHEMA.GESMarker(225)) - self.assertEqual(len(marker_list), 5) - self.assertEqual(marker_list[0].position, 223) - self.assertEqual(marker_list[1].position, 224) - self.assertEqual(marker_list[2].position, 224) - self.assertEqual(marker_list[3].position, 225) - self.assertEqual(marker_list[4].position, 226) - - def test_GstCapsFeatures_to_str(self): - features = SCHEMA.GstCapsFeatures.new_any() - string = str(features) - self.assertEqual(string, "ANY") - self.assertTrue(features.is_equivalent_to( - SCHEMA.GstCapsFeatures.new_from_str(string))) - features = SCHEMA.GstCapsFeatures( - "memory:SystemMemory", "other:az09AZ") - string = str(features) - self.assertEqual( - string, "memory:SystemMemory, other:az09AZ") - self.assertTrue(features.is_equivalent_to( - SCHEMA.GstCapsFeatures.new_from_str(string))) - - def test_serialize_string(self): - serialize = SCHEMA.GstStructure.serialize_string(UTF8_NAME) - deserialize = SCHEMA.GstStructure.deserialize_string(serialize) - self.assertEqual(deserialize, UTF8_NAME) - - def test_GstStructure_parsing(self): - struct = SCHEMA.GstStructure.new_from_str( - " properties , String-1 = ( string ) test , " - "String-2=(string)\"test\", String-3= ( string) {} , " - "Int =(int) -5 , Uint =(uint) 5 , Float-1=(float)0.5, " - "Float-2= (float ) 2, Boolean-1 =(boolean ) true, " - "Boolean-2=(boolean)No, Boolean-3=( boolean) 0 , " - "Fraction=(fraction) 2/5, Structure = (structure) " - "\"Name\\,\\ val\\=\\(string\\)\\\"test\\\\\\ test\\\"\\;\", " - "Caps =(GstCaps)\"Struct1\\(memory:SystemMemory\\)\\,\\ " - "val\\=\\(string\\)\\\"test\\\\\\ test\\\"\"," - "markers=(GESMarkerList)\"marker-times, position=(guint64)" - "2748; metadatas, val=(string)\\\"test\\\\ test\\\"; " - "marker-times, position=(guint64)1032; " - "metadatas, val=(int)-5\";" - "hidden!!!".format( - SCHEMA.GstStructure.serialize_string(UTF8_NAME)) - ) - self.assertEqual(struct.name, "properties") - self.assertEqual(struct["String-1"], "test") - self.assertEqual(struct["String-2"], "test") - self.assertEqual(struct["String-3"], UTF8_NAME) - self.assertEqual(struct["Int"], -5) - self.assertEqual(struct["Uint"], 5) - self.assertEqual(struct["Float-1"], 0.5) - self.assertEqual(struct["Float-2"], 2.0) - self.assertEqual(struct["Boolean-1"], True) - self.assertEqual(struct["Boolean-2"], False) - self.assertEqual(struct["Boolean-3"], False) - self.assertEqual(struct["Fraction"], "2/5") - self.assertTrue(struct["Structure"].is_equivalent_to( - SCHEMA.GstStructure( - "Name", {"val": ("string", "test test")}))) - self.assertTrue(struct["Caps"].is_equivalent_to( - SCHEMA.GstCaps( - SCHEMA.GstStructure( - "Struct1", {"val": ("string", "test test")}), - SCHEMA.GstCapsFeatures("memory:SystemMemory")))) - self.assertTrue(struct["markers"].is_equivalent_to( - SCHEMA.GESMarkerList( - SCHEMA.GESMarker(1032, SCHEMA.GstStructure( - "metadatas", {"val": ("int", -5)})), - SCHEMA.GESMarker(2748, SCHEMA.GstStructure( - "metadatas", {"val": ("string", "test test")}))))) - - def test_GstStructure_to_str_and_back(self): - # TODO: remove once python2 has ended - # Python2 does not have assertWarns - if str is bytes: - return - with self.assertWarns(UserWarning): - struct_before = SCHEMA.GstStructure( - "Struct:/Name0a", { - "str-ing": ("string", UTF8_NAME), - "i/nt": ("int", 67), - "f.lo+t": ("float", -0.78), - "frac_tion": ("fraction", "4/67"), - "my-type": ("mytype", "test"), - "a_list": ("list", "{ 0, 2, 1 }"), - "stru-cture": ("structure", SCHEMA.GstStructure( - "Name", {"val": ("string", UTF8_NAME)})), - "ca/ps": ("GstCaps", SCHEMA.GstCaps( - SCHEMA.GstStructure( - "Struct1", {"val": ("string", UTF8_NAME)}), - SCHEMA.GstCapsFeatures("memory:SystemMemory"))), - "markers+": ("GESMarkerList", SCHEMA.GESMarkerList( - SCHEMA.GESMarker( - 2039, SCHEMA.GstStructure( - "metadatas", - {"val": ("string", UTF8_NAME)})), - SCHEMA.GESMarker( - 209389023, SCHEMA.GstStructure( - "metadatas", - {"val": ("float", -0.96)})))) - }) - with self.assertWarns(UserWarning): - struct_after = SCHEMA.GstStructure.new_from_str( - str(struct_before)) - self.assertTrue(struct_before.is_equivalent_to(struct_after)) - - def test_GstStructure_dictionary_def(self): - struct = SCHEMA.GstStructure( - "properties", { - "String-1": ("string", "test"), - "String-2": ("string", "test space"), - "Int": ("int", -5), - "Uint": ("uint", 5), - "Float": ("float", 2.0), - "Boolean": ("boolean", True), - "Fraction": ("fraction", "2/5"), - "Structure": ("structure", SCHEMA.GstStructure( - "Name", {"val": ("string", "test space")})), - "Caps": ("GstCaps", SCHEMA.GstCaps( - SCHEMA.GstStructure( - "Struct1", - {"val": ("string", "test space")}), - SCHEMA.GstCapsFeatures("memory:SystemMemory"))), - "Markers": ("GESMarkerList", SCHEMA.GESMarkerList( - SCHEMA.GESMarker( - 2039, SCHEMA.GstStructure( - "metadatas", - {"val": ("string", "test space")})), - SCHEMA.GESMarker( - 209389023, SCHEMA.GstStructure( - "metadatas", - {"val": ("float", -0.96)})))) - } - ) - self.assertEqual(struct.name, "properties") - write = str(struct) - self.assertIn("String-1=(string)test", write) - self.assertIn("String-2=(string)\"test\\ space\"", write) - self.assertIn("Int=(int)-5", write) - self.assertIn("Uint=(uint)5", write) - self.assertIn("Float=(float)2.0", write) - self.assertIn("Boolean=(boolean)true", write) - self.assertIn("Fraction=(fraction)2/5", write) - self.assertIn( - "Structure=(structure)\"Name\\,\\ " - "val\\=\\(string\\)\\\"test\\\\\\ space\\\"\\;\"", - write) - self.assertIn( - "Caps=(GstCaps)\"Struct1\\(memory:SystemMemory\\)\\,\\ " - "val\\=\\(string\\)\\\"test\\\\\\ space\\\"\"", - write) - self.assertIn( - "Markers=(GESMarkerList)\"marker-times, position=(guint64)" - "2039; metadatas, val=(string)\\\"test\\\\ space\\\"; " - "marker-times, position=(guint64)209389023; " - "metadatas, val=(float)-0.96\"", - write) - - def test_GstStructure_equality(self): - struct1 = SCHEMA.GstStructure.new_from_str( - "name, prop1=(string)4, prop2=(int)4;") - struct2 = SCHEMA.GstStructure.new_from_str( - "name, prop2=(int)4, prop1=(string)4;") - struct3 = SCHEMA.GstStructure.new_from_str( - "name, prop1=(str)4, prop2=(gint)4;") - struct4 = SCHEMA.GstStructure.new_from_str( - "name-alt, prop1=(string)4, prop2=(int)4;") - struct5 = SCHEMA.GstStructure.new_from_str( - "name, prop1=(string)4, prop3=(int)4;") - struct6 = SCHEMA.GstStructure.new_from_str( - "name, prop1=(int)4, prop2=(int)4;") - struct7 = SCHEMA.GstStructure.new_from_str( - "name, prop1=(string)4, prop2=(int)5;") - struct8 = SCHEMA.GstStructure.new_from_str( - "name, prop1=(string)4, prop2=(int)4, prop3=(bool)true;") - struct9 = SCHEMA.GstStructure.new_from_str( - "name, prop1=(string)4;") - self.assertTrue(struct1.is_equivalent_to(struct2)) - self.assertTrue(struct1.is_equivalent_to(struct3)) - self.assertFalse(struct1.is_equivalent_to(struct4)) - self.assertFalse(struct1.is_equivalent_to(struct5)) - self.assertFalse(struct1.is_equivalent_to(struct6)) - self.assertFalse(struct1.is_equivalent_to(struct7)) - self.assertFalse(struct1.is_equivalent_to(struct8)) - self.assertFalse(struct1.is_equivalent_to(struct9)) - - def test_GstStructure_editing_string(self): - struct = SCHEMA.GstStructure.new_from_str( - 'properties, name=(string)before;') - self.assertEqual(struct["name"], "before") - struct.set("name", "string", "after") - self.assertEqual(struct["name"], "after") - self.assertEqual(str(struct), 'properties, name=(string)after;') - - def test_GstStructure_empty_string(self): - struct = SCHEMA.GstStructure.new_from_str( - 'properties, name=(string)"";') - self.assertEqual(struct["name"], "") - - def test_GstStructure_NULL_string(self): - struct = SCHEMA.GstStructure.new_from_str( - 'properties, name=(string)NULL;') - self.assertEqual(struct["name"], None) - struct = SCHEMA.GstStructure.new_from_str("properties") - struct.set("name", "string", None) - self.assertEqual(str(struct), 'properties, name=(string)NULL;') - struct = SCHEMA.GstStructure.new_from_str( - 'properties, name=(string)\"NULL\";') - self.assertEqual(struct["name"], "NULL") - self.assertEqual(str(struct), 'properties, name=(string)\"NULL\";') - - def test_GstStructure_fraction(self): - struct = SCHEMA.GstStructure.new_from_str( - 'properties, framerate=(fraction)2/5;') - self.assertEqual(struct["framerate"], "2/5") - struct.set("framerate", "fraction", Fraction("3/5")) - self.assertEqual(struct["framerate"], "3/5") - struct.set("framerate", "fraction", "4/5") - self.assertEqual(struct["framerate"], "4/5") - - def test_GstStructure_type_aliases(self): - struct = SCHEMA.GstStructure.new_from_str( - "properties,String-1=(str)test,String-2=(s)\"test\"," - "Int-1=(i)-5,Int-2=(gint)-5,Uint-1=(u)5,Uint-2=(guint)5," - "Float-1=(f)0.5,Float-2=(gfloat)0.5,Double-1=(d)0.7," - "Double-2=(gdouble)0.7,Boolean-1=(bool)true," - "Boolean-2=(b)true,Boolean-3=(gboolean)true," - "Fraction=(GstFraction)2/5," - "Structure=(GstStructure)\"name\\;\"") - self.assertEqual(struct.name, "properties") - self.assertEqual(struct["String-1"], "test") - self.assertEqual(struct["String-2"], "test") - self.assertEqual(struct["Int-1"], -5) - self.assertEqual(struct["Int-2"], -5) - self.assertEqual(struct["Uint-1"], 5) - self.assertEqual(struct["Uint-2"], 5) - self.assertEqual(struct["Float-1"], 0.5) - self.assertEqual(struct["Float-2"], 0.5) - self.assertEqual(struct["Double-1"], 0.7) - self.assertEqual(struct["Double-2"], 0.7) - self.assertEqual(struct["Boolean-1"], True) - self.assertEqual(struct["Boolean-2"], True) - self.assertEqual(struct["Boolean-3"], True) - self.assertEqual(struct["Fraction"], "2/5") - self.assertTrue(struct["Structure"].is_equivalent_to( - SCHEMA.GstStructure("name"))) - struct = SCHEMA.GstStructure("properties") - struct.set("prop", "s", "test test") - self.assertEqual(struct["prop"], "test test") - self.assertEqual(struct.get_type_name("prop"), "string") - struct.set("prop", "str", "test test") - self.assertEqual(struct["prop"], "test test") - self.assertEqual(struct.get_type_name("prop"), "string") - struct.set("prop", "i", -5) - self.assertEqual(struct["prop"], -5) - self.assertEqual(struct.get_type_name("prop"), "int") - struct.set("prop", "gint", -5) - self.assertEqual(struct["prop"], -5) - self.assertEqual(struct.get_type_name("prop"), "int") - struct.set("prop", "u", 5) - self.assertEqual(struct["prop"], 5) - self.assertEqual(struct.get_type_name("prop"), "uint") - struct.set("prop", "guint", 5) - self.assertEqual(struct["prop"], 5) - self.assertEqual(struct.get_type_name("prop"), "uint") - struct.set("prop", "f", 0.5) - self.assertEqual(struct["prop"], 0.5) - self.assertEqual(struct.get_type_name("prop"), "float") - struct.set("prop", "gfloat", 0.5) - self.assertEqual(struct["prop"], 0.5) - self.assertEqual(struct.get_type_name("prop"), "float") - struct.set("prop", "d", 0.7) - self.assertEqual(struct["prop"], 0.7) - self.assertEqual(struct.get_type_name("prop"), "double") - struct.set("prop", "gdouble", 0.7) - self.assertEqual(struct["prop"], 0.7) - self.assertEqual(struct.get_type_name("prop"), "double") - struct.set("prop", "b", True) - self.assertEqual(struct["prop"], True) - self.assertEqual(struct.get_type_name("prop"), "boolean") - struct.set("prop", "bool", True) - self.assertEqual(struct["prop"], True) - self.assertEqual(struct.get_type_name("prop"), "boolean") - struct.set("prop", "gboolean", True) - self.assertEqual(struct["prop"], True) - self.assertEqual(struct.get_type_name("prop"), "boolean") - struct.set("prop", "GstFraction", Fraction("2/5")) - self.assertEqual(struct["prop"], "2/5") - self.assertEqual(struct.get_type_name("prop"), "fraction") - struct.set("prop", "GstStructure", SCHEMA.GstStructure("name")) - self.assertTrue(struct["prop"].is_equivalent_to( - SCHEMA.GstStructure("name"))) - self.assertEqual(struct.get_type_name("prop"), "structure") - - def test_GstStructure_values_list(self): - structs = [ - SCHEMA.GstStructure.new_from_str( - "name, String1=(string)\"\", Int1=(int)0, " - "Float1=(float)0.1, Int2=(i)5, Float2=(f)0.2, " - "String2=(s)NULL, String3=(string)test"), - SCHEMA.GstStructure("name", { - "String1": ("string", ""), "Int1": ("int", 0), - "Float1": ("float", 0.1), "Int2": ("i", 5), - "Float2": ("f", 0.2), "String2": ("s", None), - "String3": ("string", "test")})] - - # TODO: remove once python2 has ended - # Python2 does not have assertCountEqual - def assertCountEqual(x, y): - if str is bytes: - self.assertEqual(sorted(x), sorted(y)) - else: - self.assertCountEqual(x, y) - - for struct in structs: - assertCountEqual( - struct.values(), ["", 0, 0.1, 5, 0.2, None, "test"]) - assertCountEqual( - struct.values_of_type("string"), ["", None, "test"]) - assertCountEqual( - struct.values_of_type("s"), ["", None, "test"]) - assertCountEqual( - struct.values_of_type("int"), [0, 5]) - assertCountEqual( - struct.values_of_type("i"), [0, 5]) - assertCountEqual( - struct.values_of_type("float"), [0.1, 0.2]) - assertCountEqual( - struct.values_of_type("f"), [0.1, 0.2]) - assertCountEqual( - struct.values_of_type("double"), []) - - def test_GstStructure_getting(self): - structs = [ - SCHEMA.GstStructure.new_from_str( - "name, String=(string)test, Int=(int)5;"), - SCHEMA.GstStructure("name", { - "String": ("string", "test"), "Int": ("int", 5)})] - for struct in structs: - self.assertEqual(struct.get("Strin"), None) - self.assertEqual(struct.get("Strin", "default"), "default") - self.assertEqual( - struct.get_typed("Strin", "string", "default"), "default") - self.assertEqual(struct.get("String"), "test") - self.assertEqual(struct.get_typed("String", "string"), "test") - self.assertEqual(struct.get_typed("String", "s"), "test") - self.assertEqual(struct.get("Int"), 5) - self.assertEqual(struct.get_typed("Int", "int"), 5) - self.assertEqual(struct.get_typed("Int", "i"), 5) - # TODO: remove once python2 has ended - # Python2 does not have assertWarns - if str is bytes: - continue - with self.assertWarns(UserWarning): - self.assertEqual( - struct.get_typed("String", "int", 23), 23) - with self.assertWarns(UserWarning): - self.assertEqual( - struct.get_typed("Int", "string", "def"), "def") - - def test_GstStructure_invalid_parse(self): - # invalid names: - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str("0name, prop=(int)4;") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str( - f"{UTF8_NAME}, prop=(int)4;") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure("0name", {"prop": ("int", 4)}) - # invalid fieldnames: - struct = SCHEMA.GstStructure.new_from_str("name") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str("name, prop erty=(int)4;") - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop erty", "int", 4) - with self.assertRaises(otio.exceptions.OTIOError): - # the following would cause problems with serializing - # followed by de-serializing, since it would create two - # different fields! - struct.set("prop=(int)4, other=", "string", "test") - # invalid type names - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str("name, prop=(my type)4;") - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "int ", 4) - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", " int", 4) - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "my type", 4) - # invalid serialized values - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str("name, prop=(int)4.5") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str("name, prop=(float)7.0s") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str('name, prop=(string);') - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str( - "name, prop=(boolean)yesyes;") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str( - "name, prop=(fraction)1/2.0;") - with self.assertRaises(otio.exceptions.OTIOError): - # no comma in list - SCHEMA.GstStructure.new_from_str( - "name, prop=(list){ 5, 6 7 };") - with self.assertRaises(otio.exceptions.OTIOError): - SCHEMA.GstStructure.new_from_str( - "name, prop=(list){ 5, 6, 7;") - # invalid setting values - with self.assertRaises(TypeError): - struct.set("prop", "int", 4.5) - with self.assertRaises(TypeError): - struct.set("prop", "float", "4.5") - with self.assertRaises(TypeError): - struct.set("prop", "string", 4) - with self.assertRaises(TypeError): - struct.set("prop", "boolean", 0) - with self.assertRaises(TypeError): - struct.set("prop", "fraction", 1) - with self.assertRaises(TypeError): - struct.set("prop", "mytype", 4) - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "mytype", "test ") - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "mytype", "&") - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "mytype", "(int)4") - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "mytype", "4, other_prop=(string)insert") - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "mytype", "4;") # would hide rest! - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "list", "{ 5, 6 7 }") # no comma - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "list", "{ {5}, { 6 7} }") # no comma - - def test_GstStructure_unknown_type(self): - # TODO: remove once python2 has ended - # Python2 does not have assertWarns - if str is bytes: - return - struct = SCHEMA.GstStructure("properties") - with self.assertRaises(otio.exceptions.OTIOError): - struct.set( - "prop", "MyType", "test, other_field=(string)insert") - # would cause errors when trying to reserialize! - with self.assertRaises(otio.exceptions.OTIOError): - struct.set("prop", "MyType ", "test ") - # don't want trailing whitespaces - with self.assertWarns(UserWarning): - struct.set("prop", "MyType", "test") - self.assertEqual(struct["prop"], "test") - with self.assertWarns(UserWarning): - struct = SCHEMA.GstStructure.new_from_str( - "properties, prop= ( MyOtherType ) 4-5 ;") - self.assertEqual(struct["prop"], "4-5") - self.assertEqual( - str(struct), "properties, prop=(MyOtherType)4-5;") - with self.assertWarns(UserWarning): - SCHEMA.GstStructure("properties", struct.fields) - with self.assertWarns(UserWarning): - struct = SCHEMA.GstStructure.new_from_str( - 'properties, prop=(string) { "spa\\ ce" , ' - '( string ) test } ;') - self.assertEqual( - struct["prop"], '{ "spa\\ ce", (string)test }') - self.assertEqual( - str(struct), 'properties, prop=(string){ "spa\\ ce", ' - '(string)test };') - with self.assertWarns(UserWarning): - struct = SCHEMA.GstStructure.new_from_str( - "properties, prop=(int)<1,3,4,5>;") - self.assertEqual(struct["prop"], "< 1, 3, 4, 5 >") - with self.assertWarns(UserWarning): - struct = SCHEMA.GstStructure.new_from_str( - "properties, prop=(int)[1,3];") - self.assertEqual(struct["prop"], "[ 1, 3 ]") - with self.assertWarns(UserWarning): - struct = SCHEMA.GstStructure.new_from_str( - "properties, prop=(MyType){(MyType){1,2}," - "(MyType){3a3,4,5}};") - self.assertEqual( - struct["prop"], - "{ (MyType){ 1, 2 }, (MyType){ 3a3, 4, 5 } }") - - def test_image_sequence_example(self): - timeline = otio.adapters.read_from_file(IMAGE_SEQUENCE_EXAMPLE_PATH) - - ges_el = self._get_xges_from_otio_timeline(timeline) - self.assertIsNotNone(ges_el) - self.assertXgesNumLayers(ges_el, 1) - self.assertXgesAsset( - ges_el, - "imagesequence:./sample_sequence/sample_sequence.%2504d.exr" + - "?rate=24/1&start-index=86400&stop-index=86450", - "GESUriClip") - - def SKIP_test_roundtrip_disk2mem2disk(self): - self.maxDiff = None - timeline = otio.adapters.read_from_file(XGES_EXAMPLE_PATH) - tmp_path = tempfile.mkstemp(suffix=".xges", text=True)[1] - - otio.adapters.write_to_file(timeline, tmp_path) - result = otio.adapters.read_from_file(tmp_path) - - original_json = otio.adapters.write_to_string(timeline, 'otio_json') - output_json = otio.adapters.write_to_string(result, 'otio_json') - self.assertMultiLineEqual(original_json, output_json) - - self.assertIsOTIOEquivalentTo(timeline, result) - - # But the xml text on disk is not identical because otio has a subset - # of features to xges and we drop all the nle specific preferences. - with open(XGES_EXAMPLE_PATH) as original_file: - with open(tmp_path) as output_file: - self.assertNotEqual(original_file.read(), output_file.read()) - - -if __name__ == '__main__': - unittest.main() diff --git a/contrib/opentimelineio_contrib/adapters/xges.py b/contrib/opentimelineio_contrib/adapters/xges.py deleted file mode 100644 index 37117679e..000000000 --- a/contrib/opentimelineio_contrib/adapters/xges.py +++ /dev/null @@ -1,3749 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright Contributors to the OpenTimelineIO project - -"""OpenTimelineIO GStreamer Editing Services XML Adapter.""" -import re -import os -import warnings -import numbers -from urllib.parse import quote -from urllib.parse import unquote -from urllib.parse import urlparse -from urllib.parse import parse_qs - -from fractions import Fraction -from xml.etree import ElementTree -from xml.dom import minidom -import itertools -import colorsys -import opentimelineio as otio - -META_NAMESPACE = "XGES" - -_TRANSITION_MAP = { - "crossfade": otio.schema.TransitionTypes.SMPTE_Dissolve -} -# Two way map -_TRANSITION_MAP.update({v: k for k, v in _TRANSITION_MAP.items()}) - - -class XGESReadError(otio.exceptions.OTIOError): - """An incorrectly formatted xges string.""" - - -class UnhandledValueError(otio.exceptions.OTIOError): - """Received value is not handled.""" - def __init__(self, name, value): - otio.exceptions.OTIOError.__init__( - self, f"Unhandled value {value!r} for {name}.") - - -class InvalidValueError(otio.exceptions.OTIOError): - """Received value is invalid.""" - def __init__(self, name, value, expect): - otio.exceptions.OTIOError.__init__( - self, "Invalid value {!r} for {}. Expect {}.".format( - value, name, expect)) - - -class DeserializeError(otio.exceptions.OTIOError): - """Receive an incorrectly serialized value.""" - MAX_LEN = 20 - - def __init__(self, read, reason): - if len(read) > self.MAX_LEN: - read = read[:self.MAX_LEN] + "..." - otio.exceptions.OTIOError.__init__( - self, "Could not deserialize the string ({}) because it {}." - "".format(read, reason)) - - -class UnhandledOtioError(otio.exceptions.OTIOError): - """Received otio object is not handled.""" - def __init__(self, otio_obj): - otio.exceptions.OTIOError.__init__( - self, "Unhandled otio schema {}.".format( - otio_obj.schema_name())) - - -def _show_ignore(msg): - """Tell user we found an error with 'msg', but we are ignoring it.""" - warnings.warn(msg + ".\nIGNORING.", stacklevel=2) - - -def _show_otio_not_supported(otio_obj, effect): - """ - Tell user that we do not properly support an otio type for 'otio_obj'. - 'effect' is a message to the user about what will happen instead. - """ - warnings.warn( - "The schema {} is not currently supported.\n{}.".format( - otio_obj.schema_name(), effect), - stacklevel=2) - - -def _wrong_type_for_arg(val, expect_type_name, arg_name): - """ - Raise exception in response to the 'arg_name' argument being given the - value 'val', when we expected it to be of the type corresponding to - 'expect_type_name'. - """ - raise TypeError( - "Expect a {} type for the '{}' argument. Received a {} type." - "".format(expect_type_name, arg_name, type(val).__name__)) - - -def _force_gst_structure_name(struct, struct_name, owner=""): - """ - If the GstStructure 'struct' does not have the given 'struct_name', - change its name to match with a warning. - 'owner' is used for the message to tell the user which object the - structure belongs to. - """ - if struct.name != struct_name: - if owner: - start = f"{owner}'s" - else: - start = "The" - warnings.warn( - "{} structure name is \"{}\" rather than the expected \"{}\"." - "\nOverwriting with the expected name.".format( - start, struct.name, struct_name)) - struct.name = struct_name - - -# TODO: remove unicode_to_str once python2 has ended: -def unicode_to_str(value): - """If python2, returns unicode as a utf8 str""" - if type(value) is not str and isinstance(value, str): - value = value.encode("utf8") - return value - - -class GESTrackType: - """ - Class for storing the GESTrackType types, and converting them to - the otio.schema.TrackKind. - """ - - UNKNOWN = 1 << 0 - AUDIO = 1 << 1 - VIDEO = 1 << 2 - TEXT = 1 << 3 - CUSTOM = 1 << 4 - OTIO_TYPES = (VIDEO, AUDIO) - NON_OTIO_TYPES = (UNKNOWN, TEXT, CUSTOM) - ALL_TYPES = OTIO_TYPES + NON_OTIO_TYPES - - @staticmethod - def to_otio_kind(track_type): - """ - Convert from GESTrackType 'track_type' to otio.schema.TrackKind. - """ - if track_type == GESTrackType.AUDIO: - return otio.schema.TrackKind.Audio - elif track_type == GESTrackType.VIDEO: - return otio.schema.TrackKind.Video - raise UnhandledValueError("track_type", track_type) - - @staticmethod - def from_otio_kind(*otio_kinds): - """ - Convert the list of otio.schema.TrackKind 'otio_kinds' to an - GESTrackType. - """ - track_type = 0 - for kind in otio_kinds: - if kind == otio.schema.TrackKind.Audio: - track_type |= GESTrackType.AUDIO - elif kind == otio.schema.TrackKind.Video: - track_type |= GESTrackType.VIDEO - else: - raise UnhandledValueError("track kind", kind) - return track_type - - -GST_SECOND = 1000000000 - - -class XGES: - """ - Class for converting an xges string, which stores GES projects, to an - otio.schema.Timeline. - """ - # The xml elements found in the given xges are converted as: - # - # + A , its , its and its s are - # converted to an otio.schema.Stack. - # + A GESMarker on the is converted to an - # otio.schema.Marker. - # + A is converted to otio.schema.Track, one for each track - # type found. - # + A + is converted to an otio.schema.Composable, one - # for each track type found: - # + A GESUriClip becomes an otio.schema.Clip with an - # otio.schema.ExternalReference. - # + A GESUriClip that references a sub-project instead becomes an - # otio.schema.Stack of the sub-project. - # + A GESTransitionClip becomes an otio.schema.Transition. - # + An on a uriclip is converted to an otio.schema.Effect. - # + An is wrapped - # - # TODO: Some parts of the xges are not converted. - # types to support: - # + GESTestClip, probably to a otio.schema.Clip with an - # otio.schema.GeneratorReference - # + GESTitleClip, maybe to a otio.schema.Clip with an - # otio.schema.MissingReference? - # + GESOverlayClip, difficult to convert since otio.schema.Clips can - # not overlap generically. Maybe use a separate otio.schema.Track? - # + GESBaseEffectClip, same difficulty. - # - # Also, for , we're missing - # + , which contains elements that describe the - # property bindings. - # - # For , we're missing: - # + , not vital. - # - # For , we're missing: - # + . - # - # For , we're missing: - # + , and its children elements. - # - # For , we're missing: - # + , same as the missing - - def __init__(self, ges_obj): - """ - 'ges_obj' should be the root of the xges xml tree (called "ges"). - If it is not an ElementTree, it will first be parsed as a string - to ElementTree. - """ - if not isinstance(ges_obj, ElementTree.Element): - ges_obj = ElementTree.fromstring(ges_obj) - if ges_obj.tag != "ges": - raise XGESReadError( - "The root element for the received xml is tagged as " - "{} rather than the expected 'ges' for xges".format( - ges_obj.tag)) - self.ges_xml = ges_obj - self.rate = 25.0 - - @staticmethod - def _findall(xmlelement, path): - """ - Return a list of all child xml elements found under 'xmlelement' - at 'path'. - """ - found = xmlelement.findall(path) - if found is None: - return [] - return found - - @classmethod - def _findonly(cls, xmlelement, path, allow_none=False): - """ - Find exactly one child xml element found under 'xmlelement' at - 'path' and return it. If we find multiple, we raise an error. If - 'allow_none' is False, we also error when we find no element, - otherwise we can return None. - """ - found = cls._findall(xmlelement, path) - if allow_none and not found: - return None - if len(found) != 1: - raise XGESReadError( - "Found {:d} xml elements under the path {} when only " - "one was expected.".format(len(found), path)) - return found[0] - - @staticmethod - def _get_attrib(xmlelement, key, expect_type): - """ - Get the xml attribute at 'key', try to convert it to the python - 'expect_type', and return it. Otherwise, raise an error. - """ - val = xmlelement.get(key) - if val is None: - raise XGESReadError( - "The xges {} element is missing the {} " - "attribute.".format(xmlelement.tag, key)) - try: - val = expect_type(val) - except (ValueError, TypeError): - raise XGESReadError( - "The xges {} element '{}' attribute has the value {}, " - "which is not of the expected {} type.".format( - xmlelement.tag, key, val, expect_type.__name__)) - return val - - @staticmethod - def _get_structure(xmlelement, attrib_name, struct_name=None): - """ - Try to find the GstStructure with the name 'struct_name' under - the 'attrib_name' attribute of 'xmlelement'. If we can not do so - we return an empty structure with the same name. If no - 'struct_name' is given, we use the 'attrib_name'. - """ - if struct_name is None: - struct_name = attrib_name - read_struct = xmlelement.get(attrib_name, struct_name + ";") - try: - struct = GstStructure.new_from_str(read_struct) - except DeserializeError as err: - _show_ignore( - "The {} attribute of {} could not be read as a " - "GstStructure:\n{!s}".format( - struct_name, xmlelement.tag, err)) - return GstStructure(struct_name) - _force_gst_structure_name(struct, struct_name, xmlelement.tag) - return struct - - @classmethod - def _get_properties(cls, xmlelement): - """Get the properties GstStructure from an xges 'xmlelement'.""" - return cls._get_structure(xmlelement, "properties") - - @classmethod - def _get_metadatas(cls, xmlelement): - """Get the metadatas GstStructure from an xges 'xmlelement'.""" - return cls._get_structure(xmlelement, "metadatas") - - @classmethod - def _get_children_properties(cls, xmlelement): - """ - Get the children-properties GstStructure from an xges - 'xmlelement'. - """ - return cls._get_structure( - xmlelement, "children-properties", "properties") - - @classmethod - def _get_from_properties( - cls, xmlelement, fieldname, expect_type, default=None): - """ - Try to get the property under 'fieldname' of the 'expect_type' - type name from the properties GstStructure of an xges element. - Otherwise return 'default'. - """ - structure = cls._get_properties(xmlelement) - return structure.get_typed(fieldname, expect_type, default) - - @classmethod - def _get_from_metadatas( - cls, xmlelement, fieldname, expect_type, default=None): - """ - Try to get the metadata under 'fieldname' of the 'expect_type' - type name from the metadatas GstStructure of an xges element. - Otherwise return 'default'. - """ - structure = cls._get_metadatas(xmlelement) - return structure.get_typed(fieldname, expect_type, default) - - @staticmethod - def _get_from_caps(caps, fieldname, structname=None, default=None): - """ - Extract a GstCaps from the 'caps' string and search it for the - first GstStructure (optionally, with the 'structname' name) with - the 'fieldname' field, and return its value. Otherwise, return - 'default'. - """ - try: - with warnings.catch_warnings(): - # unknown types may raise a warning. This will - # usually be irrelevant since we are searching for - # a specific field - caps = GstCaps.new_from_str(caps) - except DeserializeError as err: - _show_ignore( - "Failed to read the fields in the caps ({}):\n\t" - "{!s}".format(caps, err)) - else: - for struct in caps: - if structname is not None: - if struct.name != structname: - continue - # use below method rather than fields.get(fieldname) to - # allow us to want any value back, including None - for key in struct.fields: - if key == fieldname: - return struct[key] - return default - - def _set_rate_from_timeline(self, timeline): - """ - Set the rate of 'self' to the rate found in the video track - element of the xges 'timeline'. - """ - video_track = timeline.find("./track[@track-type='4']") - if video_track is None: - return - res_caps = self._get_from_properties( - video_track, "restriction-caps", "string") - if res_caps is None: - return - rate = self._get_from_caps(res_caps, "framerate") - if rate is None: - return - try: - rate = Fraction(rate) - except (ValueError, TypeError): - _show_ignore("Read a framerate that is not a fraction") - else: - self.rate = float(rate) - - def _to_rational_time(self, ns_timestamp): - """ - Converts the GstClockTime 'ns_timestamp' (nanoseconds as an int) - to an otio.opentime.RationalTime object. - """ - return otio.opentime.RationalTime( - (float(ns_timestamp) * self.rate) / float(GST_SECOND), - self.rate - ) - - @staticmethod - def _add_to_otio_metadata(otio_obj, key, val, parent_key=None): - """ - Add the data 'val' to the metadata of 'otio_obj' under 'key'. - If 'parent_key' is given, it is instead added to the - sub-dictionary found under 'parent_key'. - The needed dictionaries are automatically created. - """ - xges_dict = otio_obj.metadata.get(META_NAMESPACE) - if xges_dict is None: - otio_obj.metadata[META_NAMESPACE] = {} - xges_dict = otio_obj.metadata[META_NAMESPACE] - if parent_key is None: - _dict = xges_dict - else: - sub_dict = xges_dict.get(parent_key) - if sub_dict is None: - xges_dict[parent_key] = {} - sub_dict = xges_dict[parent_key] - _dict = sub_dict - _dict[key] = val - - @classmethod - def _add_properties_and_metadatas_to_otio( - cls, otio_obj, element, parent_key=None): - """ - Add the properties and metadatas attributes of the xges 'element' - to the metadata of 'otio_obj', as GstStructures. Optionally under - the 'parent_key'. - """ - cls._add_to_otio_metadata( - otio_obj, "properties", - cls._get_properties(element), parent_key) - cls._add_to_otio_metadata( - otio_obj, "metadatas", - cls._get_metadatas(element), parent_key) - - @classmethod - def _add_children_properties_to_otio( - cls, otio_obj, element, parent_key=None): - """ - Add the children-properties attribute of the xges 'element' to the - metadata of 'otio_obj', as GstStructures. Optionally under the - 'parent_key'. - """ - cls._add_to_otio_metadata( - otio_obj, "children-properties", - cls._get_children_properties(element), parent_key) - - def to_otio(self): - """ - Convert the xges given to 'self' to an otio.schema.Timeline - object, and returns it. - """ - otio_timeline = otio.schema.Timeline() - project = self._fill_otio_stack_from_ges(otio_timeline.tracks) - otio_timeline.name = self._get_from_metadatas( - project, "name", "string", "") - return otio_timeline - - def _fill_otio_stack_from_ges(self, otio_stack): - """ - Converts the top element given to 'self' into an - otio.schema.Stack by setting the metadata of the given - 'otio_stack', and filling it with otio.schema.Tracks. - Returns the element found under . - """ - project = self._findonly(self.ges_xml, "./project") - timeline = self._findonly(project, "./timeline") - self._set_rate_from_timeline(timeline) - self._add_timeline_markers_to_otio_stack(timeline, otio_stack) - - tracks = self._findall(timeline, "./track") - tracks.sort( - key=lambda trk: self._get_attrib(trk, "track-id", int)) - xges_tracks = [] - for track in tracks: - try: - caps = GstCaps.new_from_str( - self._get_attrib(track, "caps", str)) - except DeserializeError as err: - _show_ignore( - "Could not deserialize the caps attribute for " - "track {:d}:\n{!s}".format( - self._get_attrib(track, "track-id", int), err)) - else: - xges_tracks.append( - XgesTrack( - caps, - self._get_attrib(track, "track-type", int), - self._get_properties(track), - self._get_metadatas(track))) - - self._add_properties_and_metadatas_to_otio( - otio_stack, project, "project") - self._add_properties_and_metadatas_to_otio( - otio_stack, timeline, "timeline") - self._add_to_otio_metadata(otio_stack, "tracks", xges_tracks) - self._add_layers_to_otio_stack(timeline, otio_stack) - return project - - def _add_timeline_markers_to_otio_stack( - self, timeline, otio_stack): - """ - Add the markers found in the GESMarkerlList metadata of the xges - 'timeline' to 'otio_stack' as otio.schema.Markers. - """ - metadatas = self._get_metadatas(timeline) - for marker_list in metadatas.values_of_type("GESMarkerList"): - for marker in marker_list: - if marker.is_colored(): - otio_stack.markers.append( - self._otio_marker_from_ges_marker(marker)) - - def _otio_marker_from_ges_marker(self, ges_marker): - """Convert the GESMarker 'ges_marker' to an otio.schema.Marker.""" - with warnings.catch_warnings(): - # don't worry about not being string typed - name = ges_marker.metadatas.get_typed("comment", "string", "") - marked_range = otio.opentime.TimeRange( - self._to_rational_time(ges_marker.position), - self._to_rational_time(0)) - return otio.schema.Marker( - name=name, color=ges_marker.get_nearest_otio_color(), - marked_range=marked_range) - - def _add_layers_to_otio_stack(self, timeline, otio_stack): - """ - Add the elements under the xges 'timeline' to 'otio_stack' - as otio.schema.Tracks. - """ - sort_otio_tracks = [] - for layer in self._findall(timeline, "./layer"): - priority = self._get_attrib(layer, "priority", int) - for otio_track in self._otio_tracks_from_layer_clips(layer): - sort_otio_tracks.append((otio_track, priority)) - sort_otio_tracks.sort(key=lambda ent: ent[1], reverse=True) - # NOTE: smaller priority is later in the list - for otio_track in (ent[0] for ent in sort_otio_tracks): - otio_stack.append(otio_track) - - def _otio_tracks_from_layer_clips(self, layer): - """ - Convert the xges 'layer' into otio.schema.Tracks, one for each - otio.schema.TrackKind. - """ - otio_tracks = [] - for track_type in GESTrackType.OTIO_TYPES: - otio_items, otio_transitions = \ - self._create_otio_composables_from_layer_clips( - layer, track_type) - if not otio_items and not otio_transitions: - continue - otio_track = otio.schema.Track() - otio_track.kind = GESTrackType.to_otio_kind(track_type) - self._add_otio_composables_to_otio_track( - otio_track, otio_items, otio_transitions) - self._add_properties_and_metadatas_to_otio(otio_track, layer) - otio_tracks.append(otio_track) - for track_type in GESTrackType.NON_OTIO_TYPES: - layer_clips = self._layer_clips_for_track_type( - layer, track_type) - if layer_clips: - _show_ignore( - "The xges layer of priority {:d} contains clips " - "{!s} of the unhandled track type {:d}".format( - self._get_attrib(layer, "priority", int), - [self._get_name(clip) for clip in layer_clips], - track_type)) - return otio_tracks - - @classmethod - def _layer_clips_for_track_type(cls, layer, track_type): - """ - Return the elements found under the xges 'layer' whose - "track-types" overlaps with track_type. - """ - return [ - clip for clip in cls._findall(layer, "./clip") - if cls._get_attrib(clip, "track-types", int) & track_type] - - @classmethod - def _clip_effects_for_track_type(cls, clip, track_type): - """ - Return the elements found under the xges 'clip' whose - "track-type" matches 'track_type'. - """ - return [ - effect for effect in cls._findall(clip, "./effect") - if cls._get_attrib(effect, "track-type", int) & track_type] - # NOTE: the attribute is 'track-type', not 'track-types' - - def _create_otio_composables_from_layer_clips( - self, layer, track_type): - """ - For all the elements found in the xges 'layer' that overlap - the given 'track_type', attempt to create an - otio.schema.Composable. - - Note that the created composables do not have their timing set. - Instead, the timing information of the is stored in a - dictionary alongside the composable. - - Returns a list of otio item dictionaries, and a list of otio - transition dictionaries. - Within the item dictionary: - "item" points to the actual otio.schema.Item, - "start", "duration" and "inpoint" give the corresponding - attributes. - Within the transition dictionary: - "transition" points to the actual otio.schema.Transition, - "start" and "duration" give the corresponding - attributes. - """ - otio_transitions = [] - otio_items = [] - for clip in self._layer_clips_for_track_type(layer, track_type): - clip_type = self._get_attrib(clip, "type-name", str) - start = self._get_attrib(clip, "start", int) - inpoint = self._get_attrib(clip, "inpoint", int) - duration = self._get_attrib(clip, "duration", int) - otio_composable = None - name = self._get_name(clip) - if clip_type == "GESTransitionClip": - otio_composable = self._otio_transition_from_clip(clip) - elif clip_type == "GESUriClip": - otio_composable = self._otio_item_from_uri_clip(clip) - else: - # TODO: support other clip types - # maybe represent a GESTitleClip as a gap, with the text - # in the metadata? - # or as a clip with a MissingReference? - _show_ignore( - "The xges clip {} is of an unsupported {} type" - "".format(name, clip_type)) - continue - otio_composable.name = name - self._add_properties_and_metadatas_to_otio( - otio_composable, clip, "clip") - self._add_clip_effects_to_otio_composable( - otio_composable, clip, track_type) - if isinstance(otio_composable, otio.schema.Transition): - otio_transitions.append({ - "transition": otio_composable, - "start": start, "duration": duration}) - elif isinstance(otio_composable, otio.core.Item): - otio_items.append({ - "item": otio_composable, "start": start, - "inpoint": inpoint, "duration": duration}) - return otio_items, otio_transitions - - def _add_clip_effects_to_otio_composable( - self, otio_composable, clip, track_type): - """ - Add the elements found under the xges 'clip' of the - given 'track_type' to the 'otio_composable'. - """ - clip_effects = self._clip_effects_for_track_type( - clip, track_type) - if not isinstance(otio_composable, otio.core.Item): - if clip_effects: - _show_ignore( - "The effects {!s} found under the xges clip {} can " - "not be represented".format( - [self._get_attrib(effect, "asset-id", str) - for effect in clip_effects], - self._get_name(clip))) - return - for effect in clip_effects: - effect_type = self._get_attrib(effect, "type-name", str) - if effect_type == "GESEffect": - otio_composable.effects.append( - self._otio_effect_from_effect(effect)) - else: - _show_ignore( - "The {} effect under the xges clip {} is of an " - "unsupported {} type".format( - self._get_attrib(effect, "asset-id", str), - self._get_name(clip), effect_type)) - - def _otio_effect_from_effect(self, effect): - """Convert the xges 'effect' into an otio.schema.Effect.""" - bin_desc = self._get_attrib(effect, "asset-id", str) - # TODO: a smart way to convert the bin description into a standard - # effect name that is recognised by other adapters - # e.g. a bin description can also contain parameter values, such - # as "agingtv scratch-lines=20" - otio_effect = otio.schema.Effect(effect_name=bin_desc) - self._add_to_otio_metadata( - otio_effect, "bin-description", bin_desc) - self._add_properties_and_metadatas_to_otio(otio_effect, effect) - self._add_children_properties_to_otio(otio_effect, effect) - return otio_effect - - @staticmethod - def _item_gap(second, first): - """ - Calculate the time gap between the start time of 'second' and the - end time of 'first', each of which are item dictionaries as - returned by _create_otio_composables_from_layer_clips. - If 'first' is None, we return the gap between the start of the - timeline and the start of 'second'. - If 'second' is None, we return 0 to indicate no gap. - """ - if second is None: - return 0 - if first is None: - return second["start"] - return second["start"] - first["start"] - first["duration"] - - def _add_otio_composables_to_otio_track( - self, otio_track, items, transitions): - """ - Insert 'items' and 'transitions' into 'otio_track' with correct - timings. - - 'items' and 'transitions' should be a list of dictionaries, as - returned by _create_otio_composables_from_layer_clips. - - Specifically, the item dictionaries should contain an un-parented - otio.schema.Item under the "item" key, and GstClockTimes under the - "start", "duration" and "inpoint" keys, corresponding to the times - found under the corresponding xges . - This method should set the correct source_range for the item - before inserting it into 'otio_track'. - - The transitions dictionaries should contain an un-parented - otio.schema.Transition under the "transition" key, and - GstClockTimes under the "start" and "duration" keys, corresponding - to the times found under the corresponding xges . - Whenever an overlap of non-transition s is detected, the - transition that matches the overlap will be searched for in - 'transitions', removed from the list, and the corresponding otio - transition will be inserted in 'otio_track' with the correct - timings. - """ - # otio tracks do not allow items to overlap - # in contrast an xges layer will let clips overlap, and their - # overlap may have some corresponding transition associated with - # it. Diagrammatically, we want to translate: - # _ _ _ _ _ _ ____________ _ _ _ _ _ _ ____________ _ _ _ _ _ _ - # + + + + - # xges-clip-0 | |xges-clip-1| xges-clip-2 - # _ _ _ _ _ _+____________+_ _ _ _ _ _+____________+_ _ _ _ _ _ - # .------------. .------------. - # :xges-trans-1: :xges-trans-2: - # '------------' '------------' - # -----------> <-----------------------------------> - # start duration (on xges-clip-1) - # -----------> <----------> - # start duration (on xges-trans-1) - # ------------------------------------> <----------> - # start duration (on xges-trans-2) - # - # . . . . .......................................... - # . Not : : - # . Avail. : xges-asset for xges-clip-1 : - # . . . . .:.......................................: - # <---------> - # inpoint (on xges-clip-1) - # <----------------------------------------------> - # duration (on xges-asset) - # - # to: - # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - # + + - # otio-clip-0 | otio-clip-1 | otio-clip-2 - # _ _ _ _ _ _ _ _ _+_ _ _ _ _ _ _ _ _ _ _ _ _+_ _ _ _ _ _ _ _ _ - # .------------. .------------. - # :otio-trans-1: :otio-trans-2: - # '------------' '------------' - # <---> <----> <----> <---> - # .in_offset .out_offset .in_offset .out_offset - # - # . . . . .......................................... - # . Not : : - # . Avail. : otio-med-ref for otio-clip-1 : - # . . . . .:.......................................: - # <---------------> <-----------------------> - # s_range.start_time s_range.duration (on otio-clip-1) - # <------> <-------------------------------------> - # a_range.start_time a_range.duration (on otio-med-ref) - # - # where: - # s_range = source_range - # a_range = available_range - # - # so: - # for otio-trans-1: - # .in_offset + .out_offset = xges-trans-1-duration - # for otio-clip-1: - # s_range.start_time = xges-clip-1-inpoint - # + otio-trans-1.in_offset - # s_range.duration = xges-clip-1-duration - # - otio-trans-1.in_offset - # - otio-trans-2.out_offset - # - # - # We also want to insert any otio-gaps when the first xges clip - # does not start at zero, or if there is an implied gap between - # xges clips - items.sort(key=lambda ent: ent["start"]) - prev_otio_transition = None - for item, prev_item, next_item in zip( - items, [None] + items, items[1:] + [None]): - otio_start = self._to_rational_time(item["inpoint"]) - otio_duration = self._to_rational_time(item["duration"]) - otio_transition = None - pre_gap = self._item_gap(item, prev_item) - post_gap = self._item_gap(next_item, item) - if pre_gap < 0: - # overlap: transition should have been - # handled by the previous iteration - otio_start += prev_otio_transition.in_offset - otio_duration -= prev_otio_transition.in_offset - # start is delayed until the otio transition's position - # duration looses what start gains - elif pre_gap > 0: - otio_track.append(self._create_otio_gap(pre_gap)) - - if post_gap < 0: - # overlap - duration = -post_gap - transition = [ - t for t in transitions - if t["start"] == next_item["start"] and - t["duration"] == duration] - if len(transition) == 1: - otio_transition = transition[0]["transition"] - transitions.remove(transition[0]) - # remove transitions once they have been extracted - elif len(transition) == 0: - # NOTE: this can happen if auto-transition is false - # for the xges timeline - otio_transition = self._default_otio_transition() - else: - raise XGESReadError( - "Found {:d} {!s} transitions with start={:d} " - "and duration={:d} within a single layer".format( - len(transition), otio_track.kind, - next_item["start"], duration)) - half = float(duration) / 2.0 - otio_transition.in_offset = self._to_rational_time(half) - otio_transition.out_offset = self._to_rational_time(half) - otio_duration -= otio_transition.out_offset - # trim the end of the clip, which is where the otio - # transition starts - otio_item = item["item"] - otio_item.source_range = otio.opentime.TimeRange( - otio_start, otio_duration) - otio_track.append(otio_item) - if otio_transition: - otio_track.append(otio_transition) - prev_otio_transition = otio_transition - if transitions: - raise XGESReadError( - "xges layer contains {:d} {!s} transitions that could " - "not be associated with any clip overlap".format( - len(transitions), otio_track.kind)) - - @classmethod - def _get_name(cls, element): - """ - Get the "name" of the xges 'element' found in its properties, or - return a generic name if none is found. - """ - name = cls._get_from_properties(element, "name", "string") - if not name: - name = element.tag - return name - - def _otio_transition_from_clip(self, clip): - """ - Convert the xges transition 'clip' into an otio.schema.Transition. - Note that the timing of the object is not set. - """ - return otio.schema.Transition( - transition_type=_TRANSITION_MAP.get( - self._get_attrib(clip, "asset-id", str), - otio.schema.TransitionTypes.Custom)) - - @staticmethod - def _default_otio_transition(): - """ - Create a default otio.schema.Transition. - Note that the timing of the object is not set. - """ - return otio.schema.Transition( - transition_type=otio.schema.TransitionTypes.SMPTE_Dissolve) - - def _otio_item_from_uri_clip(self, clip): - """ - Convert the xges uri 'clip' into an otio.schema.Item. - Note that the timing of the object is not set. - - If 'clip' is found to reference a sub-project, this will return - an otio.schema.Stack of the sub-project, also converted from the - found element. - Otherwise, an otio.schema.Clip with an - otio.schema.ExternalReference is returned. - """ - asset_id = self._get_attrib(clip, "asset-id", str) - sub_project_asset = self._asset_by_id(asset_id, "GESTimeline") - if sub_project_asset is not None: - # this clip refers to a sub project - otio_item = otio.schema.Stack() - sub_ges = XGES(self._findonly(sub_project_asset, "./ges")) - sub_ges._fill_otio_stack_from_ges(otio_item) - self._add_properties_and_metadatas_to_otio( - otio_item, sub_project_asset, "sub-project-asset") - # NOTE: we include asset-id in the metadata, so that two - # stacks that refer to a single sub-project will not be - # split into separate assets when converting from - # xges->otio->xges - self._add_to_otio_metadata(otio_item, "asset-id", asset_id) - uri_clip_asset = self._asset_by_id(asset_id, "GESUriClip") - if uri_clip_asset is None: - _show_ignore( - "Did not find the expected GESUriClip asset with " - "the id {}".format(asset_id)) - else: - self._add_properties_and_metadatas_to_otio( - otio_item, uri_clip_asset, "uri-clip-asset") - else: - otio_item = otio.schema.Clip( - media_reference=self._otio_reference_from_id(asset_id)) - return otio_item - - def _create_otio_gap(self, gst_duration): - """ - Create a new otio.schema.Gap with the given GstClockTime - 'gst_duration' duration. - """ - source_range = otio.opentime.TimeRange( - self._to_rational_time(0), - self._to_rational_time(gst_duration)) - return otio.schema.Gap(source_range=source_range) - - def _otio_image_sequence_from_url(self, ref_url): - - # TODO: Add support for missing policy - params = {} - fname, ext = os.path.splitext(unquote(os.path.basename(ref_url.path))) - index_format = re.findall(r"%\d+d", fname) - if index_format: - params["frame_zero_padding"] = int(index_format[-1][2:-1]) - fname = fname[0:-len(index_format[-1])] - - url_params = parse_qs(ref_url.query) - if "framerate" in url_params: - rate = params["rate"] = float(Fraction(url_params["framerate"][-1])) - if "start-index" in url_params and "stop-index" in url_params: - start = int(url_params["start-index"][-1]) - stop = int(url_params["stop-index"][-1]) - params["available_range"] = otio.opentime.TimeRange( - otio.opentime.RationalTime(int(start), rate), - otio.opentime.RationalTime(int(stop - start), rate), - ) - else: - rate = params["rate"] = float(30) - - return otio.schema.ImageSequenceReference( - "file://" + os.path.dirname(ref_url.path), - fname, ext, **params) - - def _otio_reference_from_id(self, asset_id): - """ - Create a new otio.schema.Reference from the given 'asset_id' - of an xges . - """ - asset = self._asset_by_id(asset_id, "GESUriClip") - if asset is None: - _show_ignore( - "Did not find the expected GESUriClip asset with the " - "id {}".format(asset_id)) - return otio.schema.MissingReference() - - duration = self._get_from_properties( - asset, "duration", "guint64") - - if duration is None: - available_range = None - else: - available_range = otio.opentime.TimeRange( - start_time=self._to_rational_time(0), - duration=self._to_rational_time(duration) - ) - - ref_url = urlparse(asset_id) - if ref_url.scheme == "imagesequence": - otio_ref = self._otio_image_sequence_from_url(ref_url) - else: - otio_ref = otio.schema.ExternalReference( - target_url=asset_id, - available_range=available_range - ) - self._add_properties_and_metadatas_to_otio(otio_ref, asset) - return otio_ref - - def _asset_by_id(self, asset_id, asset_type): - """ - Return the single xges element with "id"=='asset_id' and - "extractable-type-name"=='asset_type. - """ - return self._findonly( - self.ges_xml, - "./project/ressources/asset[@id='{}']" - "[@extractable-type-name='{}']".format( - asset_id, asset_type), - allow_none=True - ) - - -class XGESOtio: - """ - Class for converting an otio.schema.Timeline into an xges string. - """ - # The otio objects found in the given timeline are converted as: - # - # + A Stack is converted to a a , its , its - # and its s. If the Stack is found underneath a Track, we - # also create a uri that references the as an - # . - # + A Track is converted to a . - # + A Clip with an ExternalReference is converted to a uri and - # an . - # + A Transition is converted to a transition . - # + An Effect on a Clip or Stack is converted to s under the - # corresponding . - # + An Effect on a Track is converted to an effect that covers - # the . - # + A Marker is converted to a GESMarker for the . - # - # TODO: Some parts of otio are not supported: - # + Clips with MissingReference or GeneratorReference references. - # The latter could probably be converted to a test . - # + The global_start_time on a Timeline is ignored. - # + TimeEffects are not converted into s or effect s. - # + We don't support a non-zero start time for uri files in xges, - # unlike MediaReference. - # + We don't have a good way to convert Effects into xges effects. - # Currently we just copy the names. - # + We don't support TimeEffects. Need to wait until xges supports - # this. - # + We don't support converting Transition transition_types into xges - # transition types. Currently they all become the default transition - # type. - - def __init__(self, input_otio=None): - """ - Initialise with the otio.schema.Timeline 'input_otio'. - """ - if input_otio is not None: - # copy the timeline so that we can freely change it - self.timeline = input_otio.deepcopy() - else: - self.timeline = None - self.all_names = set() - # map track types to a track id - self.track_id_for_type = {} - # map from a sub- element to an asset id - self.sub_projects = {} - - @staticmethod - def _rat_to_gstclocktime(rat_time): - """ - Convert an otio.opentime.RationalTime to a GstClockTime - (nanoseconds as an int). - """ - return int(otio.opentime.to_seconds(rat_time) * GST_SECOND) - - @classmethod - def _range_to_gstclocktimes(cls, time_range): - """ - Convert an otio.opentime.TimeRange to a tuple of the start_time - and duration as GstClockTimes. - """ - return (cls._rat_to_gstclocktime(time_range.start_time), - cls._rat_to_gstclocktime(time_range.duration)) - - @staticmethod - def _insert_new_sub_element(into_parent, tag, attrib=None): - """ - Create a new 'tag' xml element as a child of 'into_parent' with - the given 'attrib' attributes, and returns it. - """ - return ElementTree.SubElement(into_parent, tag, attrib or {}) - - @classmethod - def _add_properties_and_metadatas_to_element( - cls, element, otio_obj, parent_key=None, - properties=None, metadatas=None): - """ - Add the xges GstStructures "properties" and "metadatas" found in - the metadata of 'otio_obj', optionally looking under 'parent_key', - to the corresponding attributes of the xges 'element'. - If 'properties' or 'metadatas' are given, these will be used - instead of the ones found. - """ - element.attrib["properties"] = str( - properties or - cls._get_element_properties(otio_obj, parent_key)) - element.attrib["metadatas"] = str( - metadatas or - cls._get_element_metadatas(otio_obj, parent_key)) - - @classmethod - def _add_children_properties_to_element( - cls, element, otio_obj, parent_key=None, - children_properties=None): - """ - Add the xges GstStructure "children-properties" found in the - metadata of 'otio_obj', optionally looking under 'parent_key', to - the corresponding attributes of the xges 'element'. - If 'children-properties' is given, this will be used instead of - the one found. - """ - element.attrib["children-properties"] = str( - children_properties or - cls._get_element_children_properties(otio_obj, parent_key)) - - @staticmethod - def _get_from_otio_metadata( - otio_obj, key, parent_key=None, default=None): - """ - Fetch some xges data stored under 'key' from the metadata of - 'otio_obj'. If 'parent_key' is given, we fetch the data from the - dictionary under 'parent_key' in the metadata of 'otio_obj'. If - nothing was found, 'default' is returned instead. - This is used to find data that was added to 'otio_obj' using - XGES._add_to_otio_metadata. - """ - _dict = otio_obj.metadata.get(META_NAMESPACE, {}) - if parent_key is not None: - _dict = _dict.get(parent_key, {}) - return _dict.get(key, default) - - @classmethod - def _get_element_structure( - cls, otio_obj, key, struct_name, parent_key=None): - """ - Fetch a GstStructure under 'key' from the metadata of 'otio_obj', - optionally looking under 'parent_key'. - If the structure can not be found, a new empty structure with the - name 'struct_name' is created and returned instead. - This method will ensure that the returned GstStructure will have - the name 'struct_name'. - """ - struct = cls._get_from_otio_metadata( - otio_obj, key, parent_key, GstStructure(struct_name)) - _force_gst_structure_name(struct, struct_name, "{} {}".format( - type(otio_obj).__name__, otio_obj.name)) - return struct - - @classmethod - def _get_element_properties(cls, otio_obj, parent_key=None): - """ - Fetch the "properties" GstStructure under from the metadata of - 'otio_obj', optionally looking under 'parent_key'. - If the structure is not found, an empty one is returned instead. - """ - return cls._get_element_structure( - otio_obj, "properties", "properties", parent_key) - - @classmethod - def _get_element_metadatas(cls, otio_obj, parent_key=None): - """ - Fetch the "metdatas" GstStructure under from the metadata of - 'otio_obj', optionally looking under 'parent_key'. - If the structure is not found, an empty one is returned instead. - """ - return cls._get_element_structure( - otio_obj, "metadatas", "metadatas", parent_key) - - @classmethod - def _get_element_children_properties(cls, otio_obj, parent_key=None): - """ - Fetch the "children-properties" GstStructure under from the - metadata of 'otio_obj', optionally looking under 'parent_key'. - If the structure is not found, an empty one is returned instead. - """ - return cls._get_element_structure( - otio_obj, "children-properties", "properties", parent_key) - - @staticmethod - def _set_structure_value(struct, field, _type, value): - """ - For the given GstStructure 'struct', set the value under 'field' - to 'value' with the given type name '_type'. - If the type name is different from the current type name for - 'field', the value is still set, but we also issue a warning. - """ - if field in struct.fields: - current_type = struct.get_type_name(field) - if current_type != _type: - # the type changing is unexpected - warnings.warn( - "The structure {} has a {} typed value {!s} under {}." - "\nOverwriting with the {} typed value {!s}".format( - struct.name, current_type, - struct.get_value(field), field, _type, value)) - struct.set(field, _type, value) - - @staticmethod - def _asset_exists(asset_id, ressources, *extract_types): - """ - Test whether we have already created the xges under the - xges 'ressources' with id 'asset_id', and matching one of the - 'extract_types'. - """ - assets = ressources.findall("./asset") - if asset_id is None or assets is None: - return False - for extract_type in extract_types: - for asset in assets: - if asset.get("extractable-type-name") == extract_type \ - and asset.get("id") == asset_id: - return True - return False - - @classmethod - def _xges_element_equal(cls, first_el, second_el): - """Test if 'first_el' is equal to 'second_el'.""" - # start with most likely failures - if first_el.attrib != second_el.attrib: - return False - if len(first_el) != len(second_el): - return False - if first_el.tag != second_el.tag: - return False - # zip should be safe for comparison since we've already checked - # for equal length - for first_child, second_child in zip(first_el, second_el): - if not cls._xges_element_equal(first_child, second_child): - return False - if first_el.text != second_el.text: - return False - if first_el.tail != second_el.tail: - return False - return True - - def _serialize_stack_to_ressource(self, otio_stack, ressources): - """ - Use 'otio_stack' to create a new xges under the xges - 'ressources' corresponding to a sub-project. If the asset already - exists, it is not created. In either case, returns the asset id - for the corresponding . - """ - sub_obj = XGESOtio() - sub_ges = sub_obj._serialize_stack_to_ges(otio_stack) - for existing_sub_ges in self.sub_projects: - if self._xges_element_equal(existing_sub_ges, sub_ges): - # Already have the sub project as an asset, so return its - # asset id - return self.sub_projects[existing_sub_ges] - asset_id = self._get_from_otio_metadata(otio_stack, "asset-id") - if not asset_id: - asset_id = otio_stack.name or "sub-project" - orig_asset_id = asset_id - for i in itertools.count(start=1): - if not self._asset_exists( - asset_id, ressources, "GESUriClip", "GESTimeline"): - # NOTE: asset_id must be unique for both the - # GESTimeline and GESUriClip extractable types - break - asset_id = orig_asset_id + f"_{i:d}" - # create a timeline asset - asset = self._insert_new_sub_element( - ressources, "asset", attrib={ - "id": asset_id, "extractable-type-name": "GESTimeline"}) - self._add_properties_and_metadatas_to_element( - asset, otio_stack, "sub-project-asset") - asset.append(sub_ges) - self.sub_projects[sub_ges] = asset_id - - # also create a uri asset for the clip - uri_asset = self._insert_new_sub_element( - ressources, "asset", attrib={ - "id": asset_id, "extractable-type-name": "GESUriClip"}) - self._add_properties_and_metadatas_to_element( - uri_asset, otio_stack, "uri-clip-asset") - return asset_id - - def _serialize_external_reference_to_ressource( - self, reference, ressources): - """ - Use the the otio.schema.ExternalReference 'reference' to create - a new xges under the xges 'ressources' corresponding to a - uri clip asset. If the asset already exists, it is not created. - """ - if isinstance(reference, otio.schema.ImageSequenceReference): - base_url = urlparse(reference.target_url_base) - asset_id = "imagesequence:" + base_url.path - if not base_url.path.endswith("/"): - asset_id += "/" - asset_id += quote( - reference.name_prefix + "%0" - + str(reference.frame_zero_padding) - + "d" + reference.name_suffix) - - params = [] - if reference.rate: - rate = reference.rate.as_integer_ratio() - params.append("rate=%i/%i" % (rate[0], rate[1])) - - if reference.available_range: - params.append( - "start-index=%i" % - int(reference.available_range.start_time.value)) - params.append( - "stop-index=%i" % ( - reference.available_range.start_time.value - + reference.available_range.duration.value)) - - if params: - asset_id += '?' - asset_id += '&'.join(params) - else: - asset_id = reference.target_url - if self._asset_exists(asset_id, ressources, "GESUriClip"): - return asset_id - properties = self._get_element_properties(reference) - if properties.get_typed("duration", "guint64") is None: - a_range = reference.available_range - if a_range is not None: - self._set_structure_value( - properties, "duration", "guint64", - sum(self._range_to_gstclocktimes(a_range))) - # TODO: check that this is correct approach for when - # start_time is not 0. - # duration is the sum of the a_range start_time and - # duration we ignore that frames before start_time are - # not available - asset = self._insert_new_sub_element( - ressources, "asset", attrib={ - "id": asset_id, "extractable-type-name": "GESUriClip"}) - self._add_properties_and_metadatas_to_element( - asset, reference, properties=properties) - return asset_id - - @classmethod - def _get_effect_bin_desc(cls, otio_effect): - """ - Get the xges effect bin-description property from 'otio_effect'. - """ - bin_desc = cls._get_from_otio_metadata( - otio_effect, "bin-description") - if bin_desc is None: - # TODO: have a smart way to convert an effect name into a bin - # description - warnings.warn( - "Did not find a GESEffect bin-description for the {0} " - "effect. Using \"{0}\" as the bin-description." - "".format(otio_effect.effect_name)) - bin_desc = otio_effect.effect_name - return bin_desc - - def _serialize_item_effect( - self, otio_effect, clip, clip_id, track_type): - """ - Convert 'otio_effect' into a 'track_type' xges under the - xges 'clip' with the given 'clip_id'. - """ - if isinstance(otio_effect, otio.schema.TimeEffect): - _show_otio_not_supported(otio_effect, "Ignoring") - return - track_id = self.track_id_for_type.get(track_type) - if track_id is None: - _show_ignore( - "Could not get the required track-id for the {} effect " - "because no xges track with the track-type {:d} exists" - "".format(otio_effect.effect_name, track_type)) - return - effect = self._insert_new_sub_element( - clip, "effect", attrib={ - "asset-id": str(self._get_effect_bin_desc(otio_effect)), - "clip-id": str(clip_id), - "type-name": "GESEffect", - "track-type": str(track_type), - "track-id": str(track_id) - } - ) - self._add_properties_and_metadatas_to_element(effect, otio_effect) - self._add_children_properties_to_element(effect, otio_effect) - - def _serialize_item_effects( - self, otio_item, clip, clip_id, track_types): - """ - Place all the effects found on 'otio_item' that overlap - 'track_types' under the xges 'clip' with the given 'clip_id'. - """ - for track_type in ( - t for t in GESTrackType.ALL_TYPES if t & track_types): - for otio_effect in otio_item.effects: - self._serialize_item_effect( - otio_effect, clip, clip_id, track_type) - - def _serialize_track_effect_to_effect_clip( - self, otio_effect, layer, layer_priority, start, duration, - track_types, clip_id): - """ - Convert the effect 'otio_effect' found on an otio.schema.Track - into a GESEffectClip xges under the xges 'layer' with the - given 'layer_priority'. 'start', 'duration', 'clip_id' and - 'track-types' will be used for the corresponding attributes of the - . - """ - if isinstance(otio_effect, otio.schema.TimeEffect): - _show_otio_not_supported(otio_effect, "Ignoring") - return - self._insert_new_sub_element( - layer, "clip", attrib={ - "id": str(clip_id), - "asset-id": str(self._get_effect_bin_desc(otio_effect)), - "type-name": "GESEffectClip", - "track-types": str(track_types), - "layer-priority": str(layer_priority), - "start": str(start), - "rate": '0', - "inpoint": "0", - "duration": str(duration), - "properties": "properties;", - "metadatas": "metadatas;" - } - ) - # TODO: add properties and metadatas if we support converting - # GESEffectClips to otio track effects - - def _get_properties_with_unique_name( - self, named_otio, parent_key=None): - """ - Find the xges "properties" GstStructure found in the metadata of - 'named_otio', optionally under 'parent_key'. If the "name" - property is not found or not unique for the project, it is - modified to make it so. Then the structure is returned. - """ - properties = self._get_element_properties(named_otio, parent_key) - name = properties.get_typed("name", "string") - if not name: - name = named_otio.name or named_otio.schema_name() - tmpname = name - for i in itertools.count(start=1): - if tmpname not in self.all_names: - break - tmpname = name + f"_{i:d}" - self.all_names.add(tmpname) - self._set_structure_value(properties, "name", "string", tmpname) - return properties - - def _get_clip_times( - self, otio_composable, prev_composable, next_composable, - prev_otio_end): - """ - Convert the timing of 'otio_composable' into an xges - times, using the previous object in the parent otio.schema.Track - 'prev_composable', the next object in the track 'next_composable', - and the end time of 'prev_composable' in GstClockTime - 'prev_otio_end', as references. 'next_composable' and - 'prev_composable' may be None when no such sibling exists. - 'prev_otio_end' should be the 'otio_end' that was returned from - this method for 'prev_composable', or the initial time of the - xges . - - Returns the "start", "duration" and "inpoint" attributes for the - , as well as the end time of 'otio_composable', all in - the coordinates of the xges and in GstClockTimes. - """ - # see _add_otio_composables_to_track for the translation from - # xges clips to otio clips. Here we reverse this by setting: - # for xges-trans-1: - # otio_end = prev_otio_end - # start = prev_otio_end - # - otio-trans-1.in_offset - # duration = otio-trans-1.in_offset - # + otio-trans-1.out_offset - # - # for xges-clip-1: - # otio_end = prev_otio_end - # + otio-clip-1.s_range.duration - # start = prev_otio_end - # - otio-clip-1.in_offset - # duration = otio-clip-1.s_range.duration - # + otio-trans-1.in_offset - # + otio-trans-2.out_offset - # inpoint = otio-clip-1.s_range.start_time - # - otio-trans-1.in_offset - if isinstance(otio_composable, otio.core.Item): - otio_start_time, otio_duration = self._range_to_gstclocktimes( - otio_composable.trimmed_range()) - otio_end = prev_otio_end + otio_duration - start = prev_otio_end - duration = otio_duration - inpoint = otio_start_time - if isinstance(prev_composable, otio.schema.Transition): - in_offset = self._rat_to_gstclocktime( - prev_composable.in_offset) - start -= in_offset - duration += in_offset - inpoint -= in_offset - if isinstance(next_composable, otio.schema.Transition): - duration += self._rat_to_gstclocktime( - next_composable.out_offset) - elif isinstance(otio_composable, otio.schema.Transition): - otio_end = prev_otio_end - in_offset = self._rat_to_gstclocktime( - otio_composable.in_offset) - out_offset = self._rat_to_gstclocktime( - otio_composable.out_offset) - start = prev_otio_end - in_offset - duration = in_offset + out_offset - inpoint = 0 - else: - # NOTE: core schemas only give Item and Transition as - # composable types - raise UnhandledOtioError(otio_composable) - return start, duration, inpoint, otio_end - - def _serialize_composable_to_clip( - self, otio_composable, prev_composable, next_composable, - layer, layer_priority, track_types, ressources, clip_id, - prev_otio_end): - """ - Convert 'otio_composable' into an xges with the id - 'clip_id', under the xges 'layer' with 'layer_priority'. The - previous object in the parent otio.schema.Track - 'prev_composable', the next object in the track 'next_composable', - and the end time of 'prev_composable' in GstClockTime - 'prev_otio_end', are used as references. Any xges - elements needed for the are placed under the xges - 'ressources'. - - 'next_composable' and 'prev_composable' may be None when no such - sibling exists. 'prev_otio_end' should be the 'otio_end' that was - returned from this method for 'prev_composable', or the initial - time of the xges . 'clip_id' should be the 'clip_id' - that was returned from this method for 'prev_composable', or 0 - for the first clip. - - Note that a new clip may not be created for some otio types, such - as otio.schema.Gaps, but the timings will be updated to accomodate - them. - - Returns the 'clip_id' for the next clip, and the end time of - 'otio_composable' in the coordinates of the xges in - GstClockTime. - """ - start, duration, inpoint, otio_end = self._get_clip_times( - otio_composable, prev_composable, next_composable, - prev_otio_end) - - asset_id = None - asset_type = None - if isinstance(otio_composable, otio.schema.Gap): - pass - elif isinstance(otio_composable, otio.schema.Transition): - asset_type = "GESTransitionClip" - # FIXME: get transition type from metadata if transition is - # not supported by otio - # currently, any Custom_Transition is being turned into a - # crossfade - asset_id = _TRANSITION_MAP.get( - otio_composable.transition_type, "crossfade") - elif isinstance(otio_composable, otio.schema.Clip): - ref = otio_composable.media_reference - if ref is None or ref.is_missing_reference: - pass # treat as a gap - # FIXME: properly handle missing reference - elif isinstance(ref, - (otio.schema.ExternalReference, - otio.schema.ImageSequenceReference)): - asset_type = "GESUriClip" - asset_id = self._serialize_external_reference_to_ressource( - ref, ressources) - elif isinstance(ref, otio.schema.MissingReference): - pass # shouldn't really happen - elif isinstance(ref, otio.schema.GeneratorReference): - # FIXME: insert a GESTestClip if possible once otio - # supports GeneratorReferenceTypes - _show_otio_not_supported( - ref, "Treating as a gap") - else: - _show_otio_not_supported( - ref, "Treating as a gap") - elif isinstance(otio_composable, otio.schema.Stack): - asset_id = self._serialize_stack_to_ressource( - otio_composable, ressources) - asset_type = "GESUriClip" - else: - _show_otio_not_supported(otio_composable, "Treating as a gap") - - if asset_id is None: - if isinstance(prev_composable, otio.schema.Transition) \ - or isinstance(next_composable, otio.schema.Transition): - # unassigned clip is preceded or followed by a transition - # transitions in GES are only between two clips, so - # we will insert an empty GESTitleClip to act as a - # transparent clip, which emulates an otio gap - asset_id = "GESTitleClip" - asset_type = "GESTitleClip" - # else gap is simply the absence of a clip - if asset_id is None: - # No clip is inserted, so return same clip_id - return (clip_id, otio_end) - - clip = self._insert_new_sub_element( - layer, "clip", attrib={ - "id": str(clip_id), - "asset-id": str(asset_id), - "type-name": str(asset_type), - "track-types": str(track_types), - "layer-priority": str(layer_priority), - "start": str(start), - "rate": '0', - "inpoint": str(inpoint), - "duration": str(duration), - } - ) - self._add_properties_and_metadatas_to_element( - clip, otio_composable, "clip", - properties=self._get_properties_with_unique_name( - otio_composable, "clip")) - if isinstance(otio_composable, otio.core.Item): - self._serialize_item_effects( - otio_composable, clip, clip_id, track_types) - return (clip_id + 1, otio_end) - - def _serialize_stack_to_tracks(self, otio_stack, timeline): - """ - Create the xges elements for the xges 'timeline' using - 'otio_stack'. - """ - xges_tracks = self._get_from_otio_metadata(otio_stack, "tracks") - if xges_tracks is None: - xges_tracks = [] - # FIXME: track_id is currently arbitrarily set. - # Only the xges effects, source and bindings elements use - # a track-id attribute, which are not yet supported anyway. - track_types = self._get_stack_track_types(otio_stack) - for track_type in GESTrackType.OTIO_TYPES: - if track_types & track_type: - xges_tracks.append( - XgesTrack.new_from_track_type(track_type)) - for track_id, xges_track in enumerate(xges_tracks): - track_type = xges_track.track_type - self._insert_new_sub_element( - timeline, "track", - attrib={ - "caps": str(xges_track.caps), - "track-type": str(track_type), - "track-id": str(track_id), - "properties": str(xges_track.properties), - "metadatas": str(xges_track.metadatas) - }) - if track_type in self.track_id_for_type: - warnings.warn( - "More than one XgesTrack was found with the same " - "track type {0:d}.\nAll xges elements with " - "track-type={0:d} (such as effects) will use " - "track-id={1:d}.".format( - track_type, self.track_id_for_type[track_type])) - else: - self.track_id_for_type[track_type] = track_id - - def _serialize_track_to_layer( - self, otio_track, timeline, layer_priority): - """ - Convert 'otio_track' into an xges for the xges 'timeline' - with the given 'layer_priority'. The layer is not yet filled with - clips. - """ - layer = self._insert_new_sub_element( - timeline, "layer", - attrib={"priority": str(layer_priority)}) - self._add_properties_and_metadatas_to_element(layer, otio_track) - return layer - - def _serialize_stack_to_project( - self, otio_stack, ges, otio_timeline): - """ - Convert 'otio_stack' into an xges for the xges 'ges' - element. 'otio_timeline' should be the otio.schema.Timeline that - 'otio_stack' belongs to, or None if 'otio_stack' is a sub-stack. - """ - metadatas = self._get_element_metadatas(otio_stack, "project") - if not metadatas.get_typed("name", "string"): - if otio_timeline is not None and otio_timeline.name: - self._set_structure_value( - metadatas, "name", "string", otio_timeline.name) - elif otio_stack.name: - self._set_structure_value( - metadatas, "name", "string", otio_stack.name) - project = self._insert_new_sub_element(ges, "project") - self._add_properties_and_metadatas_to_element( - project, otio_stack, "project", metadatas=metadatas) - return project - - @staticmethod - def _already_have_marker_at_position( - position, color, comment, marker_list): - """ - Test whether we already have a GESMarker in the GESMarkerList - 'marker_list' at the given 'position', approximately of the given - otio.schema.MarkerColor 'color' and with the given 'comment'. - """ - comment = comment or None - for marker in marker_list.markers_at_position(position): - if marker.get_nearest_otio_color() == color and \ - marker.metadatas.get("comment") == comment: - return True - return False - - def _put_otio_marker_into_marker_list(self, otio_marker, marker_list): - """ - Translate the otio.schema.Marker 'otio_marker' into a GESMarker - and place it in the GESMarkerList 'marker_list' if it is not - suspected to be a duplicate. - If the duration of 'otio_marker' is not 0, up to two markers can - be put in 'marker_list': one for the start time and one for the - end time. - """ - start, dur = self._range_to_gstclocktimes(otio_marker.marked_range) - if dur: - positions = (start, start + dur) - else: - positions = (start, ) - for position in positions: - name = otio_marker.name - if not self._already_have_marker_at_position( - position, otio_marker.color, name, marker_list): - ges_marker = GESMarker(position) - ges_marker.set_color_from_otio_color(otio_marker.color) - if name: - ges_marker.metadatas.set( - "comment", "string", name) - marker_list.add(ges_marker) - - def _serialize_stack_to_timeline(self, otio_stack, project): - """ - Convert 'otio_stack' into an xges under the xges - 'project', and return it. The timeline is not filled. - """ - timeline = self._insert_new_sub_element(project, "timeline") - metadatas = self._get_element_metadatas(otio_stack, "timeline") - if otio_stack.markers: - marker_list = metadatas.get_typed("markers", "GESMarkerList") - if marker_list is None: - lists = metadatas.values_of_type("GESMarkerList") - if lists: - marker_list = max(lists, key=lambda lst: len(lst)) - if marker_list is None: - self._set_structure_value( - metadatas, "markers", "GESMarkerList", GESMarkerList()) - marker_list = metadatas.get("markers") - for otio_marker in otio_stack.markers: - self._put_otio_marker_into_marker_list( - otio_marker, marker_list) - self._add_properties_and_metadatas_to_element( - timeline, otio_stack, "timeline", metadatas=metadatas) - return timeline - - def _serialize_stack_to_ges(self, otio_stack, otio_timeline=None): - """ - Convert 'otio_stack' into an xges and return it. - 'otio_timeline' should be the otio.schema.Timeline that - 'otio_stack' belongs to, or None if 'otio_stack' is a sub-stack. - """ - ges = ElementTree.Element("ges", version="0.6") - project = self._serialize_stack_to_project( - otio_stack, ges, otio_timeline) - ressources = self._insert_new_sub_element(project, "ressources") - timeline = self._serialize_stack_to_timeline(otio_stack, project) - self._serialize_stack_to_tracks(otio_stack, timeline) - - clip_id = 0 - for layer_priority, otio_track in enumerate(reversed(otio_stack)): - # NOTE: stack orders tracks with later tracks having higher - # priority, so we reverse the list for xges - layer = self._serialize_track_to_layer( - otio_track, timeline, layer_priority) - # FIXME: should the start be effected by the global_start_time - # on the otio timeline? - otio_end = 0 - track_types = self._get_track_types(otio_track) - for otio_composable in otio_track: - neighbours = otio_track.neighbors_of(otio_composable) - clip_id, otio_end = self._serialize_composable_to_clip( - otio_composable, neighbours[0], neighbours[1], - layer, layer_priority, track_types, ressources, - clip_id, otio_end) - if otio_track.effects: - min_start = None - max_end = 0 - for clip in layer: - start = int(clip.get("start")) - end = start + int(clip.get("duration")) - if min_start is None or start < min_start: - min_start = start - if end > max_end: - max_end = end - if min_start is None: - min_start = 0 - for otio_effect in otio_track.effects: - self._serialize_track_effect_to_effect_clip( - otio_effect, layer, layer_priority, min_start, - max_end - min_start, track_types, clip_id) - clip_id += 1 - return ges - - @staticmethod - def _remove_non_xges_metadata(otio_obj): - """Remove non-xges metadata from 'otio_obj.'""" - keys = [k for k in otio_obj.metadata.keys()] - for key in keys: - if key != META_NAMESPACE: - del otio_obj.metadata[key] - - @staticmethod - def _add_track_types(otio_track, track_type): - """ - Append the given 'track_type' to the metadata of 'otio_track'. - """ - otio_track.metadata["track-types"] |= track_type - - @staticmethod - def _set_track_types(otio_track, track_type): - """Set the given 'track_type' on the metadata of 'otio_track.""" - otio_track.metadata["track-types"] = track_type - - @staticmethod - def _get_track_types(otio_track): - """ - Get the track types that we set on the metadata of 'otio_track'. - """ - return otio_track.metadata["track-types"] - - @classmethod - def _get_stack_track_types(cls, otio_stack): - """Get the xges track types corresponding to 'otio_stack'.""" - track_types = 0 - for otio_track in otio_stack: - track_types |= cls._get_track_types(otio_track) - return track_types - - @classmethod - def _init_track_types(cls, otio_track): - """Initialise the track type metadat on 'otio_track'.""" - # May overwrite the metadata, but we have a deepcopy of the - # original timeline and track-type is not otherwise used. - cls._set_track_types( - otio_track, GESTrackType.from_otio_kind(otio_track.kind)) - - @classmethod - def _merge_track_in_place(cls, otio_track, merge): - """ - Merge the otio.schema.Track 'merge' into 'otio_track'. - Note that the two tracks should be equal, modulo their track kind. - """ - cls._add_track_types(otio_track, cls._get_track_types(merge)) - - @classmethod - def _equal_track_modulo_kind(cls, otio_track, compare): - """ - Test whether 'otio_track' is equivalent to 'compare', ignoring - any difference in their otio.schema.TrackKind. - """ - otio_track_types = cls._get_track_types(otio_track) - compare_track_types = cls._get_track_types(compare) - if otio_track_types & compare_track_types: - # do not want to merge two tracks if they overlap in - # their track types. Otherwise, we may "loose" a track - # after merging - return False - tmp_kind = compare.kind - compare.kind = otio_track.kind - cls._set_track_types(compare, otio_track_types) - same = otio_track.is_equivalent_to(compare) - compare.kind = tmp_kind - cls._set_track_types(compare, compare_track_types) - return same - - @classmethod - def _merge_tracks_in_stack(cls, otio_stack): - """ - Merge equivalent tracks found in the stack, modulo their track - kind. - """ - index = len(otio_stack) - 1 # start with higher priority - while index > 0: - track = otio_stack[index] - next_track = otio_stack[index - 1] - if cls._equal_track_modulo_kind(track, next_track): - # want to merge if two tracks are the same, except their - # track kind is *different* - # merge down - cls._merge_track_in_place(next_track, track) - del otio_stack[index] - # next track will be the merged one, which allows - # us to merge again. Currently this is redundant since - # there are only two track kinds - index -= 1 - - @classmethod - def _pad_source_range_track(cls, otio_stack): - """ - Go through the children of 'otio_stack'. If we find an - otio.schema.Track with a set source_range, we replace it with an - otio.schema.Track with no source_range. This track will have only - one child, which will be an otio.schema.Stack with the same - source_range. This stack will have only one child, which will be - the original track. - This is done because the source_range of a track is ignored when - converting to xges, but the source_range of a stack is not. - """ - index = 0 - while index < len(otio_stack): - # we are using this form of iteration to make transparent - # that we may be editing the stack's content - child = otio_stack[index] - if isinstance(child, otio.schema.Track) and \ - child.source_range is not None: - # each track will correspond to a layer, but xges can - # not trim a layer, so to account for the source_range, - # we will place the layer below a clip by using - # sub-projects. - # i.e. we will insert above a track and stack, where the - # stack takes the source_range instead - new_track = otio.schema.Track( - name=child.name, - kind=child.kind) - cls._init_track_types(new_track) - new_stack = otio.schema.Stack( - name=child.name, - source_range=child.source_range) - child.source_range = None - otio_stack[index] = new_track - new_track.append(new_stack) - new_stack.append(child) - index += 1 - - @staticmethod - def _pad_double_track(otio_track): - """ - If we find another otio.schema.Track under 'otio_track', we - replace it with an otio.schema.Stack that contains the previous - track as a single child. - This is done because the conversion to xges expects to only find - non-tracks under a track. - """ - index = 0 - while index < len(otio_track): - # we are using this form of iteration to make transparent - # that we may be editing the track's content - child = otio_track[index] - if isinstance(child, otio.schema.Track): - # have two tracks in a row, we expect tracks to be - # below a stack, so we will insert a stack inbetween - insert = otio.schema.Stack(name=child.name) - otio_track[index] = insert - insert.append(child) - index += 1 - - @classmethod - def _pad_non_track_children_of_stack(cls, otio_stack): - """ - If we find a child of 'otio_stack' that is not an - otio.schema.Track, we replace it with a new otio.schema.Track - that contains the previous child as its own single child. - This is done because the conversion to xges expects to only find - tracks under a stack. - """ - index = 0 - while index < len(otio_stack): - # we are using this form of iteration to make transparent - # that we may be editing the stack's content - child = otio_stack[index] - if not isinstance(child, otio.schema.Track): - # we expect a stack to only contain tracks, so we will - # insert a track inbetween - insert = otio.schema.Track(name=child.name) - if isinstance(child, otio.schema.Stack): - cls._set_track_types( - insert, cls._get_stack_track_types(child)) - else: - warnings.warn( - "Found an otio {} object directly under a " - "Stack.\nTreating as a Video and Audio source." - "".format(child.schema_name())) - cls._set_track_types( - insert, GESTrackType.VIDEO | GESTrackType.AUDIO) - otio_stack[index] = insert - insert.append(child) - index += 1 - - @staticmethod - def _move_markers_into(from_otio, into_otio): - """Move the markers found in 'from_otio' into 'into_otio'.""" - for otio_marker in from_otio.markers: - otio_marker.marked_range = from_otio.transformed_time_range( - otio_marker.marked_range, into_otio) - into_otio.markers.append(otio_marker) - if hasattr(from_otio.markers, "clear"): - from_otio.markers.clear() - else: - # TODO: remove below when python2 has ended - # markers has no clear method - while from_otio.markers: - from_otio.markers.pop() - - @classmethod - def _move_markers_to_stack(cls, otio_stack): - """ - Move all the otio.schema.Markers found in the children of - 'otio_stack' into itself. - """ - for otio_track in otio_stack: - cls._move_markers_into(otio_track, otio_stack) - for otio_composable in otio_track: - if isinstance(otio_composable, otio.core.Item) and \ - not isinstance(otio_composable, otio.schema.Stack): - cls._move_markers_into(otio_composable, otio_stack) - - @classmethod - def _perform_bottom_up(cls, func, otio_composable, filter_type): - """ - Perform the given 'func' on all otio composables of the given - 'filter_type' that are found below the given 'otio_composable'. - - This works from the lowest child upwards. - - The given function 'func' should accept a single argument, and - should not change the number or order of siblings within the - arguments's parent, but it is OK to change the children of the - argument. - """ - if isinstance(otio_composable, otio.core.Composition): - for child in otio_composable: - cls._perform_bottom_up(func, child, filter_type) - if isinstance(otio_composable, filter_type): - func(otio_composable) - - def _prepare_timeline(self): - """ - Prepare the timeline given to 'self' for conversion to xges, by - placing it in a desired format. - """ - if self.timeline.tracks.source_range is not None or \ - self.timeline.tracks.effects: - # only xges clips can correctly handle a trimmed - # source_range, so place this stack one layer down. Note - # that a dummy track will soon be inserted between these - # two stacks - # - # if the top stack contains effects, we do the same so that - # we can simply apply the effects to the clip - orig_stack = self.timeline.tracks.deepcopy() - # seem to get an error if we don't copy the stack - self.timeline.tracks = otio.schema.Stack() - self.timeline.tracks.name = orig_stack.name - self.timeline.tracks.append(orig_stack) - # get rid of non-xges metadata. In particular, this will allow - # two otio objects to look the same if they only differ by some - # unused metadata - self._perform_bottom_up( - self._remove_non_xges_metadata, - self.timeline.tracks, otio.core.SerializableObject) - # this needs to be first, to give all tracks the required - # metadata. Any tracks created after this must manually set - # this metadata - self._perform_bottom_up( - self._init_track_types, - self.timeline.tracks, otio.schema.Track) - self._perform_bottom_up( - self._pad_double_track, - self.timeline.tracks, otio.schema.Track) - self._perform_bottom_up( - self._pad_non_track_children_of_stack, - self.timeline.tracks, otio.schema.Stack) - # the next operations must be after the previous ones, to ensure - # that all stacks only contain tracks as items - self._perform_bottom_up( - self._pad_source_range_track, - self.timeline.tracks, otio.schema.Stack) - self._perform_bottom_up( - self._merge_tracks_in_stack, - self.timeline.tracks, otio.schema.Stack) - self._perform_bottom_up( - self._move_markers_to_stack, - self.timeline.tracks, otio.schema.Stack) - - def to_xges(self): - """ - Convert the otio.schema.Timeline given to 'self' into an xges - string. - """ - self._prepare_timeline() - ges = self._serialize_stack_to_ges( - self.timeline.tracks, self.timeline) - # with indentations. - string = ElementTree.tostring(ges, encoding="UTF-8") - dom = minidom.parseString(string) - return dom.toprettyxml(indent=' ') - - -# -------------------- -# adapter requirements -# -------------------- -def read_from_string(input_str): - """ - Necessary read method for otio adapter - - Args: - input_str (str): A GStreamer Editing Services formated project - - Returns: - OpenTimeline: An OpenTimeline object - """ - - return XGES(input_str).to_otio() - - -def write_to_string(input_otio): - """ - Necessary write method for otio adapter - - Args: - input_otio (OpenTimeline): An OpenTimeline object - - Returns: - str: The string contents of an FCP X XML - """ - - return XGESOtio(input_otio).to_xges() - - -# -------------------- -# schemas -# -------------------- - -@otio.core.register_type -class GstStructure(otio.core.SerializableObject): - """ - An OpenTimelineIO Schema that acts as a named dictionary with - typed entries, essentially mimicking the GstStructure of the - GStreamer C library. - - In particular, this schema mimics the gst_structure_to_string and - gst_structure_from_string C methods. As such, it can be used to - read and write the properties and metadatas attributes found in - xges elements. - - Note that the types are to correspond to GStreamer/GES GTypes, - rather than python types. - - Current supported GTypes: - GType Associated Accepted - Python type aliases - ====================================== - gint int int, i - glong int - gint64 int - guint int uint, u - gulong int - guint64 int - gfloat float float, f - gdouble float double, d - gboolean bool boolean, - bool, b - string str or None str, s - GstFraction str or fraction - Fraction - GstStructure GstStructure structure - schema - GstCaps GstCaps - schema - GESMarkerList GESMarkerList - schema - - Note that other types can be given: these must be given as strings - and the user will be responsible for making sure they are already in - a serialized form. - """ - _serializable_label = "GstStructure.1" - - name = otio.core.serializable_field( - "name", str, "The name of the structure") - fields = otio.core.serializable_field( - "fields", dict, "The fields of the structure, of the form:\n" - " {fielname: (type, value), ...}\n" - "where 'fieldname' is a str that names the field, 'type' is " - "a str that names the value type, and 'value' is the actual " - "value. Note that the name of the type corresponds to the " - "GType that would be used in the Gst/GES library, or some " - "accepted alias, rather than the python type.") - - INT_TYPES = ("int", "glong", "gint64") - UINT_TYPES = ("uint", "gulong", "guint64") - FLOAT_TYPES = ("float", "double") - BOOLEAN_TYPE = "boolean" - FRACTION_TYPE = "fraction" - STRING_TYPE = "string" - STRUCTURE_TYPE = "structure" - CAPS_TYPE = "GstCaps" - MARKER_LIST_TYPE = "GESMarkerList" - KNOWN_TYPES = INT_TYPES + UINT_TYPES + FLOAT_TYPES + ( - BOOLEAN_TYPE, FRACTION_TYPE, STRING_TYPE, STRUCTURE_TYPE, - CAPS_TYPE, MARKER_LIST_TYPE) - - TYPE_ALIAS = { - "i": "int", - "gint": "int", - "u": "uint", - "guint": "uint", - "f": "float", - "gfloat": "float", - "d": "double", - "gdouble": "double", - "b": BOOLEAN_TYPE, - "bool": BOOLEAN_TYPE, - "gboolean": BOOLEAN_TYPE, - "GstFraction": FRACTION_TYPE, - "str": STRING_TYPE, - "s": STRING_TYPE, - "GstStructure": STRUCTURE_TYPE - } - - def __init__(self, name=None, fields=None): - otio.core.SerializableObject.__init__(self) - if name is None: - name = "Unnamed" - if fields is None: - fields = {} - name = unicode_to_str(name) - if type(name) is not str: - _wrong_type_for_arg(name, "str", "name") - self._check_name(name) - self.name = name - try: - fields = dict(fields) - except (TypeError, ValueError): - _wrong_type_for_arg(fields, "dict", "fields") - self.fields = {} - for key in fields: - entry = fields[key] - if type(entry) is not tuple: - try: - entry = tuple(entry) - except (TypeError, ValueError): - raise TypeError( - "Expect dict to be filled with tuple-like " - "entries") - if len(entry) != 2: - raise TypeError( - "Expect dict to be filled with 2-entry tuples") - self.set(key, *entry) - - def __repr__(self): - return f"GstStructure({self.name!r}, {self.fields!r})" - - UNKNOWN_PREFIX = "[UNKNOWN]" - - @classmethod - def _make_type_unknown(cls, _type): - return cls.UNKNOWN_PREFIX + _type - # note the sqaure brackets make the type break the TYPE_FORMAT - - @classmethod - def _is_unknown_type(cls, _type): - return _type[:len(cls.UNKNOWN_PREFIX)] == cls.UNKNOWN_PREFIX - - @classmethod - def _get_unknown_type(cls, _type): - return _type[len(cls.UNKNOWN_PREFIX):] - - def _field_to_str(self, key): - """Return field in a serialized form""" - _type, value = self.fields[key] - _type = unicode_to_str(_type) - key = unicode_to_str(key) - value = unicode_to_str(value) - if type(key) is not str: - raise TypeError("Found a key that is not a str type") - if type(_type) is not str: - raise TypeError( - "Found a type name that is not a str type") - self._check_key(key) - _type = self.TYPE_ALIAS.get(_type, _type) - if self._is_unknown_type(_type): - _type = self._get_unknown_type(_type) - self._check_type(_type) - self._check_unknown_typed_value(value) - # already in serialized form - else: - self._check_type(_type) - value = self.serialize_value(_type, value) - return f"{key}=({_type}){value}" - - def _fields_to_str(self): - write = [] - for key in self.fields: - write.append(f", {self._field_to_str(key)}") - return "".join(write) - - def _name_to_str(self): - """Return the name in a serialized form""" - name = unicode_to_str(self.name) - self._check_name(name) - return name - - def __str__(self): - """Emulates gst_structure_to_string""" - return f"{self._name_to_str()}{self._fields_to_str()};" - - def get_type_name(self, key): - """Return the field type""" - _type = self.fields[key][0] - _type = unicode_to_str(_type) - return _type - - def get_value(self, key): - """Return the field value""" - value = self.fields[key][1] - value = unicode_to_str(value) - return value - - def __getitem__(self, key): - return self.get_value(key) - - def __len__(self): - return len(self.fields) - - @staticmethod - def _val_type_err(typ, val, expect): - raise TypeError( - "Received value ({!s}) is a {} rather than a {}, even " - "though the {} type was given".format( - val, type(val).__name__, expect, typ)) - - def set(self, key, _type, value): - """Set a field to the given typed value""" - key = unicode_to_str(key) - _type = unicode_to_str(_type) - value = unicode_to_str(value) - if type(key) is not str: - _wrong_type_for_arg(key, "str", "key") - if type(_type) is not str: - _wrong_type_for_arg(_type, "str", "_type") - _type = self.TYPE_ALIAS.get(_type, _type) - if self.fields.get(key) == (_type, value): - return - self._check_key(key) - type_is_unknown = True - if self._is_unknown_type(_type): - # this can happen if the user is setting a GstStructure - # using a preexisting GstStructure, the type will then - # be passed and marked as unknown - _type = self._get_unknown_type(_type) - self._check_type(_type) - else: - self._check_type(_type) - if _type in self.INT_TYPES: - type_is_unknown = False - # TODO: simply check for int once python2 has ended - # currently in python2, can receive either an int or - # a long - if not isinstance(value, numbers.Integral): - self._val_type_err(_type, value, "int") - elif _type in self.UINT_TYPES: - type_is_unknown = False - # TODO: simply check for int once python2 has ended - # currently in python2, can receive either an int or - # a long - if not isinstance(value, numbers.Integral): - self._val_type_err(_type, value, "int") - if value < 0: - raise InvalidValueError( - "value", value, "a positive integer for {} " - "types".format(_type)) - elif _type in self.FLOAT_TYPES: - type_is_unknown = False - if type(value) is not float: - self._val_type_err(_type, value, "float") - elif _type == self.BOOLEAN_TYPE: - type_is_unknown = False - if type(value) is not bool: - self._val_type_err(_type, value, "bool") - elif _type == self.FRACTION_TYPE: - type_is_unknown = False - if type(value) is Fraction: - value = str(value) # store internally as a str - elif type(value) is str: - try: - Fraction(value) - except ValueError: - raise InvalidValueError( - "value", value, "a fraction for the {} " - "types".format(_type)) - else: - self._val_type_err(_type, value, "Fraction or str") - elif _type == self.STRING_TYPE: - type_is_unknown = False - if value is not None and type(value) is not str: - self._val_type_err(_type, value, "str or None") - elif _type == self.STRUCTURE_TYPE: - type_is_unknown = False - if not isinstance(value, GstStructure): - self._val_type_err(_type, value, "GstStructure") - elif _type == self.CAPS_TYPE: - type_is_unknown = False - if not isinstance(value, GstCaps): - self._val_type_err(_type, value, "GstCaps") - elif _type == self.MARKER_LIST_TYPE: - type_is_unknown = False - if not isinstance(value, GESMarkerList): - self._val_type_err(_type, value, "GESMarkerList") - if type_is_unknown: - self._check_unknown_typed_value(value) - warnings.warn( - "The GstStructure type {} with the value ({}) is " - "unknown. The value will be stored and serialized as " - "given.".format(_type, value)) - _type = self._make_type_unknown(_type) - self.fields[key] = (_type, value) - # NOTE: in python2, otio will convert a str value to a unicode - - def get(self, key, default=None): - """Return the raw value associated with key""" - if key in self.fields: - value = self.get_value(key) - value = unicode_to_str(value) - return value - return default - - def get_typed(self, key, expect_type, default=None): - """ - Return the raw value associated with key if its type matches. - Raises a warning if a value exists under key but is of the - wrong type. - """ - expect_type = unicode_to_str(expect_type) - if type(expect_type) is not str: - _wrong_type_for_arg(expect_type, "str", "expect_type") - expect_type = self.TYPE_ALIAS.get(expect_type, expect_type) - if key in self.fields: - type_name = self.get_type_name(key) - if expect_type == type_name: - value = self.get_value(key) - value = unicode_to_str(value) - return value - warnings.warn( - "The structure {} contains a value under {}, but is " - "a {}, rather than the expected {} type".format( - self.name, key, type_name, expect_type)) - return default - - def values(self): - """Return a list of all values contained in the structure""" - return [self.get_value(key) for key in self.fields] - - def values_of_type(self, _type): - """ - Return a list of all values contained of the given type in the - structure - """ - _type = unicode_to_str(_type) - if type(_type) is not str: - _wrong_type_for_arg(_type, "str", "_type") - _type = self.TYPE_ALIAS.get(_type, _type) - return [self.get_value(key) for key in self.fields - if self.get_type_name(key) == _type] - - ASCII_SPACES = r"(\\?[ \t\n\r\f\v])*" - END_FORMAT = r"(?P" + ASCII_SPACES + r")" - NAME_FORMAT = r"(?P[a-zA-Z][a-zA-Z0-9/_.:-]*)" - # ^Format requirement for the name of a GstStructure - SIMPLE_STRING = r"[a-zA-Z0-9_+/:.-]+" - # see GST_ASCII_CHARS (below) - KEY_FORMAT = r"(?P" + SIMPLE_STRING + r")" - # NOTE: GstStructure technically allows more general keys, but - # these can break the parsing. - TYPE_FORMAT = r"(?P" + SIMPLE_STRING + r")" - BASIC_VALUE_FORMAT = \ - r'(?P("(\\.|[^"])*")|(' + SIMPLE_STRING + r'))' - # consume simple string or a string between quotes. Second will - # consume anything that is escaped, including a '"' - # NOTE: \\. is used rather than \\" since: - # + '"start\"end;"' should be captured as '"start\"end"' since - # the '"' is escaped. - # + '"start\\"end;"' should be captured as '"start\\"' since the - # '\' is escaped, not the '"' - # In the fist case \\. will consume '\"', and in the second it will - # consumer '\\', as desired. The second would not work with just \\" - - # TODO: remove the trailing '$' when python2 has ended and use - # re's fullmatch rather than match (not available in python2) - - @staticmethod - def _check_against_regex(check, regex, name): - # TODO: once python2 has ended, use 'fullmatch' - if not regex.match(check): - raise InvalidValueError( - name, check, "to match the regular expression {}" - "".format(regex.pattern)) - - # TODO: once python2 has ended, we can drop the trailing $ and use - # re.fullmatch in _check_against_regex - NAME_REGEX = re.compile(NAME_FORMAT + "$") - KEY_REGEX = re.compile(KEY_FORMAT + "$") - TYPE_REGEX = re.compile(TYPE_FORMAT + "$") - - @classmethod - def _check_name(cls, name): - cls._check_against_regex(name, cls.NAME_REGEX, "name") - - @classmethod - def _check_key(cls, key): - cls._check_against_regex(key, cls.KEY_REGEX, "key") - - @classmethod - def _check_type(cls, _type): - cls._check_against_regex(_type, cls.TYPE_REGEX, "type") - - @classmethod - def _check_unknown_typed_value(cls, value): - if type(value) is not str: - cls._val_type_err("unknown", value, "string") - try: - # see if the value could be successfully parsed in again - ret_type, ret_val, _ = cls._parse_value(value, False) - except DeserializeError as err: - raise InvalidValueError( - "value", value, "unknown-typed values to be in a " - "serialized format ({!s})".format(err)) - else: - if ret_type is not None: - raise InvalidValueError( - "value", value, "unknown-typed values to *not* " - "start with a type specification, only the " - "serialized value should be given") - if ret_val != value: - raise InvalidValueError( - "value", value, "unknown-typed values to be the " - "same as its parsed value {}".format(ret_val)) - - PARSE_NAME_REGEX = re.compile( - ASCII_SPACES + NAME_FORMAT + END_FORMAT) - - @classmethod - def _parse_name(cls, read): - match = cls.PARSE_NAME_REGEX.match(read) - if match is None: - raise DeserializeError( - read, "does not start with a correct name") - name = match.group("name") - read = read[match.end("end"):] - return name, read - - @classmethod - def _parse_range_list_array(cls, read): - start = read[0] - end = {'[': ']', '{': '}', '<': '>'}.get(start) - read = read[1:] - values = [start, ' '] - first = True - while read and read[0] != end: - if first: - first = False - else: - if read and read[0] != ',': - DeserializeError( - read, "does not contain a comma between listed " - "items") - values.append(", ") - read = read[1:] - _type, value, read = cls._parse_value(read, False) - if _type is not None: - if cls._is_unknown_type(_type): - # remove unknown marker for serialization - _type = cls._get_unknown_type(_type) - values.extend(('(', _type, ')')) - values.append(value) - if not read: - raise DeserializeError( - read, f"ended before {end} could be found") - read = read[1:] # skip past 'end' - match = cls.END_REGEX.match(read) # skip whitespace - read = read[match.end("end"):] - # NOTE: we are ignoring the incorrect cases where a range - # has 0, 1 or 4+ values! This is the users responsiblity. - values.extend((' ', end)) - return "".join(values), read - - FIELD_START_REGEX = re.compile( - ASCII_SPACES + KEY_FORMAT + ASCII_SPACES + r"=" + END_FORMAT) - FIELD_TYPE_REGEX = re.compile( - ASCII_SPACES + r"(\(" + ASCII_SPACES + TYPE_FORMAT - + ASCII_SPACES + r"\))?" + END_FORMAT) - FIELD_VALUE_REGEX = re.compile( - ASCII_SPACES + BASIC_VALUE_FORMAT + END_FORMAT) - END_REGEX = re.compile(END_FORMAT) - - @classmethod - def _parse_value(cls, read, deserialize=True): - match = cls.FIELD_TYPE_REGEX.match(read) - # match shouldn't be None since the (TYPE_FORMAT) is optional - # and the rest is just ASCII_SPACES - _type = match.group("type") - if _type is None and deserialize: - # if deserialize is False, the (type) is optional - raise DeserializeError( - read, "does not contain a valid '(type)' format") - _type = cls.TYPE_ALIAS.get(_type, _type) - type_is_unknown = True - read = read[match.end("end"):] - if read and read[0] in ('[', '{', '<'): - # range/list/array types - # this is an unknown type, even though _type itself may - # be known. e.g. a list on integers will have _type as 'int' - # but the corresponding value can not be deserialized as an - # integer - value, read = cls._parse_range_list_array(read) - if deserialize: - # prevent printing on subsequent calls if we find a - # list within a list, etc. - warnings.warn( - "GstStructure received a range/list/array of type " - "{}, which can not be deserialized. Storing the " - "value as {}.".format(_type, value)) - else: - match = cls.FIELD_VALUE_REGEX.match(read) - if match is None: - raise DeserializeError( - read, "does not have a valid value format") - read = read[match.end("end"):] - value = match.group("value") - if deserialize: - if _type in cls.KNOWN_TYPES: - type_is_unknown = False - try: - value = cls.deserialize_value(_type, value) - except DeserializeError as err: - raise DeserializeError( - read, "contains an invalid typed value " - "({!s})".format(err)) - else: - warnings.warn( - "GstStructure found a type {} that is unknown. " - "The corresponding value ({}) will not be " - "deserialized and will be stored as given." - "".format(_type, value)) - if type_is_unknown and _type is not None: - _type = cls._make_type_unknown(_type) - return _type, value, read - - @classmethod - def _parse_field(cls, read): - match = cls.FIELD_START_REGEX.match(read) - if match is None: - raise DeserializeError( - read, "does not have a valid 'key=...' format") - key = match.group("key") - read = read[match.end("end"):] - _type, value, read = cls._parse_value(read) - return key, _type, value, read - - @classmethod - def _parse_fields(cls, read): - read = unicode_to_str(read) - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - fields = {} - while read and read[0] != ';': - if read and read[0] != ',': - DeserializeError( - read, "does not separate fields with commas") - read = read[1:] - key, _type, value, read = cls._parse_field(read) - fields[key] = (_type, value) - if read: - # read[0] == ';' - read = read[1:] - return fields, read - - @classmethod - def new_from_str(cls, read): - """ - Returns a new instance of GstStructure, based on the Gst library - function gst_structure_from_string. - Strings obtained from the GstStructure str() method can be - parsed in to recreate the original GstStructure. - """ - read = unicode_to_str(read) - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - name, read = cls._parse_name(read) - fields = cls._parse_fields(read)[0] - return GstStructure(name=name, fields=fields) - - @staticmethod - def _val_read_err(typ, val): - raise DeserializeError( - val, f"does not translated to the {typ} type") - - @classmethod - def deserialize_value(cls, _type, value): - """Return the value as the corresponding type""" - _type = unicode_to_str(_type) - if type(_type) is not str: - _wrong_type_for_arg(_type, "str", "_type") - value = unicode_to_str(value) - if type(value) is not str: - _wrong_type_for_arg(value, "str", "value") - _type = cls.TYPE_ALIAS.get(_type, _type) - if _type in cls.INT_TYPES or _type in cls.UINT_TYPES: - try: - value = int(value) - except ValueError: - cls._val_read_err(_type, value) - if _type in cls.UINT_TYPES and value < 0: - cls._val_read_err(_type, value) - elif _type in cls.FLOAT_TYPES: - try: - value = float(value) - except ValueError: - cls._val_read_err(_type, value) - elif _type == cls.BOOLEAN_TYPE: - try: - value = cls.deserialize_boolean(value) - except DeserializeError: - cls._val_read_err(_type, value) - elif _type == cls.FRACTION_TYPE: - try: - value = str(Fraction(value)) # store internally as a str - except ValueError: - cls._val_read_err(_type, value) - elif _type == cls.STRING_TYPE: - try: - value = cls.deserialize_string(value) - except DeserializeError as err: - raise DeserializeError( - value, "does not translate to a string ({!s})" - "".format(err)) - elif _type == cls.STRUCTURE_TYPE: - try: - value = cls.deserialize_structure(value) - except DeserializeError as err: - raise DeserializeError( - value, "does not translate to a GstStructure ({!s})" - "".format(err)) - elif _type == cls.CAPS_TYPE: - try: - value = cls.deserialize_caps(value) - except DeserializeError as err: - raise DeserializeError( - value, "does not translate to a GstCaps ({!s})" - "".format(err)) - elif _type == cls.MARKER_LIST_TYPE: - try: - value = cls.deserialize_marker_list(value) - except DeserializeError as err: - raise DeserializeError( - value, "does not translate to a GESMarkerList " - "({!s})".format(err)) - else: - raise ValueError( - "The type {} is unknown, so the value ({}) can not " - "be deserialized.".format(_type, value)) - return value - - @classmethod - def serialize_value(cls, _type, value): - """Serialize the typed value as a string""" - _type = unicode_to_str(_type) - if type(_type) is not str: - _wrong_type_for_arg(_type, "str", "_type") - value = unicode_to_str(value) - _type = cls.TYPE_ALIAS.get(_type, _type) - if _type in cls.INT_TYPES + cls.UINT_TYPES + cls.FLOAT_TYPES \ - + (cls.FRACTION_TYPE, ): - return str(value) - if _type == cls.BOOLEAN_TYPE: - return cls.serialize_boolean(value) - if _type == cls.STRING_TYPE: - return cls.serialize_string(value) - if _type == cls.STRUCTURE_TYPE: - return cls.serialize_structure(value) - if _type == cls.CAPS_TYPE: - return cls.serialize_caps(value) - if _type == cls.MARKER_LIST_TYPE: - return cls.serialize_marker_list(value) - raise ValueError( - "The type {} is unknown, so the value ({}) can not be " - "serialized.".format(_type, str(value))) - - # see GST_ASCII_IS_STRING in gst_private.h - GST_ASCII_CHARS = [ - ord(letter) for letter in - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "0123456789" - "_-+/:." - ] - LEADING_OCTAL_CHARS = [ord(letter) for letter in "0123"] - OCTAL_CHARS = [ord(letter) for letter in "01234567"] - - @classmethod - def serialize_string(cls, value): - """ - Emulates gst_value_serialize_string. - Accepts a bytes, str or None type. - Returns a str type. - """ - if value is not None and type(value) is not str: - _wrong_type_for_arg(value, "None or str", "value") - return cls._wrap_string(value) - - @classmethod - def _wrap_string(cls, read): - if read is None: - return "NULL" - if read == "NULL": - return "\"NULL\"" - if type(read) is bytes: - # NOTE: in python2 this will be True if read is a str type - # in python3 it will not - pass - elif type(read) is str: - read = read.encode() - else: - _wrong_type_for_arg(read, "None, str, or bytes", "read") - if not read: - return '""' - added_wrap = False - ser_string_list = [] - for byte in bytearray(read): - # For python3 we could have just called `byte in read` - # For python2 we need the `bytearray(read)` cast to convert - # the str type to int - # TODO: simplify once python2 has ended - if byte in cls.GST_ASCII_CHARS: - ser_string_list.append(chr(byte)) - elif byte < 0x20 or byte >= 0x7f: - ser_string_list.append(f"\\{byte:03o}") - added_wrap = True - else: - ser_string_list.append("\\" + chr(byte)) - added_wrap = True - if added_wrap: - ser_string_list.insert(0, '"') - ser_string_list.append('"') - return "".join(ser_string_list) - - @classmethod - def deserialize_string(cls, read): - """ - Emulates gst_value_deserialize_string. - Accepts a str type. - Returns a str or None type. - """ - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - if read == "NULL": - return None - if not read: - return "" - if read[0] != '"' or read[-1] != '"': - return read - return cls._unwrap_string(read) - - @classmethod - def _unwrap_string(cls, read): - """Emulates gst_string_unwrap""" - if type(read) is bytes: - # TODO: remove once python2 has ended - read_array = bytearray(read) - else: - read_array = bytearray(read.encode()) - byte_list = [] - bytes_iter = iter(read_array) - - def next_byte(): - try: - return next(bytes_iter) - except StopIteration: - raise DeserializeError(read, "end unexpectedly") - - byte = next_byte() - if byte != ord('"'): - raise DeserializeError( - read, "does not start with '\"', but ends with '\"'") - while True: - byte = next_byte() - if byte in cls.GST_ASCII_CHARS: - byte_list.append(byte) - elif byte == ord('"'): - try: - next(bytes_iter) - except StopIteration: - # expect there to be no more bytes - break - raise DeserializeError( - read, "contains an un-escaped '\"' before the end") - elif byte == ord('\\'): - byte = next_byte() - if byte in cls.LEADING_OCTAL_CHARS: - # could be the start of an octal - byte2 = next_byte() - byte3 = next_byte() - if byte2 in cls.OCTAL_CHARS and byte3 in cls.OCTAL_CHARS: - nums = [b - ord('0') for b in (byte, byte2, byte3)] - byte = (nums[0] << 6) + (nums[1] << 3) + nums[2] - byte_list.append(byte) - else: - raise DeserializeError( - read, "contains the start of an octal " - "sequence but not the end") - else: - if byte == 0: - raise DeserializeError( - read, "contains a null byte after an escape") - byte_list.append(byte) - else: - raise DeserializeError( - read, "contains an unexpected un-escaped character") - out_str = bytes(bytearray(byte_list)) - if type(out_str) is str: - # TODO: remove once python2 has ended - # and simplify above to only call bytes(byte_list) - return out_str - try: - return out_str.decode() - except (UnicodeError, ValueError): - raise DeserializeError( - read, "contains invalid utf-8 byte sequences") - - @staticmethod - def serialize_boolean(value): - """ - Emulates gst_value_serialize_boolean. - Accepts bool type. - Returns a str type. - """ - if type(value) is not bool: - _wrong_type_for_arg(value, "bool", "value") - if value: - return "true" - return "false" - - @staticmethod - def deserialize_boolean(read): - """ - Emulates gst_value_deserialize_boolean. - Accepts str type. - Returns a bool type. - """ - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - if read.lower() in ("true", "t", "yes", "1"): - return True - if read.lower() in ("false", "f", "no", "0"): - return False - raise DeserializeError(read, "is an unknown boolean value") - - @classmethod - def serialize_structure(cls, value): - """ - Emulates gst_value_serialize_structure. - Accepts a GstStructure. - Returns a str type. - """ - if not isinstance(value, GstStructure): - _wrong_type_for_arg(value, "GstStructure", "value") - return cls._wrap_string(str(value)) - - @classmethod - def deserialize_structure(cls, read): - """ - Emulates gst_value_serialize_structure. - Accepts a str type. - Returns a GstStructure. - """ - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - if read[0] == '"': - # NOTE: since all GstStructure strings end with ';', we - # don't ever expect the above to *not* be true, but the - # GStreamer library allows for this case - try: - read = cls._unwrap_string(read) - # NOTE: in the GStreamer library, serialized - # GstStructure and GstCaps strings are sent to - # _priv_gst_value_parse_string with unescape set to - # TRUE. What this essentially does is replace "\x" with - # just "x". Since caps and structure strings should only - # contain printable ascii characters before they are - # passed to _wrap_string, this should be equivalent to - # calling _unwrap_string. Our method is more clearly a - # reverse of the serialization method. - except DeserializeError as err: - raise DeserializeError( - read, "could not be unwrapped as a string ({!s})" - "".format(err)) - return GstStructure.new_from_str(read) - - @classmethod - def serialize_caps(cls, value): - """ - Emulates gst_value_serialize_caps. - Accepts a GstCaps. - Returns a str type. - """ - if not isinstance(value, GstCaps): - _wrong_type_for_arg(value, "GstCaps", "value") - return cls._wrap_string(str(value)) - - @classmethod - def deserialize_caps(cls, read): - """ - Emulates gst_value_serialize_caps. - Accepts a str type. - Returns a GstCaps. - """ - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - if read[0] == '"': - # can be not true if a caps only contains a single empty - # structure, or is ALL or NONE - try: - read = cls._unwrap_string(read) - except DeserializeError as err: - raise DeserializeError( - read, "could not be unwrapped as a string ({!s})" - "".format(err)) - return GstCaps.new_from_str(read) - - @classmethod - def serialize_marker_list(cls, value): - """ - Emulates ges_marker_list_serialize. - Accepts a GESMarkerList. - Returns a str type. - """ - if not isinstance(value, GESMarkerList): - _wrong_type_for_arg(value, "GESMarkerList", "value") - caps = GstCaps() - for marker in value.markers: - caps.append(GstStructure( - "marker-times", - {"position": ("guint64", marker.position)})) - caps.append(marker.metadatas) - # NOTE: safe to give the metadatas to the caps since we - # will not be using caps after this function - # i.e. the caller will still have essential ownership of - # the matadatas - return cls._escape_string(str(caps)) - - @staticmethod - def _escape_string(read): - """ - Emulates some of g_strescape's behaviour in - ges_marker_list_serialize - """ - # NOTE: in the original g_strescape, all the special characters - # '\b', '\f', '\n', '\r', '\t', '\v', '\' and '"' are escaped, - # and all characters in the range 0x01-0x1F and non-ascii - # characters are replaced by an octal sequence - # (similar to _wrap_string). - # However, a caps string should only contain printable ascii - # characters, so it should be sufficient to simply escape '\' - # and '"'. - escaped = ['"'] - for character in read: - if character in ('"', '\\'): - escaped.append('\\') - escaped.append(character) - escaped.append('"') - return "".join(escaped) - - @classmethod - def deserialize_marker_list(cls, read): - """ - Emulates ges_marker_list_deserialize. - Accepts a str type. - Returns a GESMarkerList. - """ - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - read = cls._unescape_string(read) - # Above is actually performed by _priv_gst_value_parse_value, - # but it is called immediately before gst_value_deserialize - caps = GstCaps.new_from_str(read) - if len(caps) % 2: - raise DeserializeError( - read, "does not contain an even-sized caps") - position = None - marker_list = GESMarkerList() - for index, (struct, _) in enumerate(caps.structs): - if index % 2 == 0: - if struct.name != "marker-times": - raise DeserializeError( - read, "contains a structure named {} rather " - "than the expected \"marker-times\"".format( - struct.name)) - if "position" not in struct.fields: - raise DeserializeError( - read, "is missing a position value") - if struct.get_type_name("position") != "guint64": - raise DeserializeError( - read, "does not have a guint64 typed position") - position = struct["position"] - else: - marker_list.add(GESMarker(position, struct)) - return marker_list - - @staticmethod - def _unescape_string(read): - """ - Emulates behaviour of _priv_gst_value_parse_string with - unescape set to TRUE. This should undo _escape_string - """ - if read[0] != '"': - return read - character_iter = iter(read) - - def next_char(): - try: - return next(character_iter) - except StopIteration: - raise DeserializeError(read, "ends unexpectedly") - - next_char() # skip '"' - unescaped = [] - while True: - character = next_char() - if character == '"': - break - if character == '\\': - unescaped.append(next_char()) - else: - unescaped.append(character) - return "".join(unescaped) - - -@otio.core.register_type -class GstCapsFeatures(otio.core.SerializableObject): - """ - An OpenTimelineIO Schema that contains a collection of features, - mimicking a GstCapsFeatures of the Gstreamer C libarary. - """ - _serializable_label = "GstCapsFeatures.1" - is_any = otio.core.serializable_field( - "is_any", bool, "Whether a GstCapsFeatures matches any. If " - "True, then features must be empty.") - features = otio.core.serializable_field( - "features", list, "A list of features, as strings") - - def __init__(self, *features): - """ - Initialize the GstCapsFeatures. - - 'features' should be a series of feature names as strings. - """ - otio.core.SerializableObject.__init__(self) - self.is_any = False - self.features = [] - for feature in features: - feature = unicode_to_str(feature) - if type(feature) is not str: - _wrong_type_for_arg(feature, "strs", "features") - self._check_feature(feature) - self.features.append(feature) - # NOTE: if 'features' is a str, rather than a list of strs - # then this will iterate through all of its characters! But, - # a single character can not match the feature regular - # expression. - - def __getitem__(self, index): - return self.features[index] - - def __len__(self): - return len(self.features) - - @classmethod - def new_any(cls): - features = cls() - features.is_any = True - return features - - # Based on gst_caps_feature_name_is_valid - FEATURE_FORMAT = r"(?P[a-zA-Z]*:[a-zA-Z][a-zA-Z0-9]*)" - # TODO: once python2 has ended, we can drop the trailing $ and use - # re.fullmatch in _check_feature - FEATURE_REGEX = re.compile(FEATURE_FORMAT + "$") - - @classmethod - def _check_feature(cls, feature): - # TODO: once python2 has ended, use 'fullmatch' - if not cls.FEATURE_REGEX.match(feature): - raise InvalidValueError( - "feature", feature, "to match the regular expression " - "{}".format(cls.FEATURE_REGEX.pattern)) - - PARSE_FEATURE_REGEX = re.compile( - r" *" + FEATURE_FORMAT + "(?P)") - - @classmethod - def new_from_str(cls, read): - """ - Returns a new instance of GstCapsFeatures, based on the Gst - library function gst_caps_features_from_string. - Strings obtained from the GstCapsFeatures str() method can be - parsed in to recreate the original GstCapsFeatures. - """ - read = unicode_to_str(read) - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - if read == "ANY": - return cls.new_any() - first = True - features = [] - while read: - if first: - first = False - else: - if read[0] != ',': - DeserializeError( - read, "does not separate features with commas") - read = read[1:] - match = cls.PARSE_FEATURE_REGEX.match(read) - if match is None: - raise DeserializeError( - read, "does not match the regular expression {}" - "".format(cls.PARSE_FEATURE_REGEX.pattern)) - features.append(match.group("feature")) - read = read[match.end("end"):] - return cls(*features) - - def __repr__(self): - if self.is_any: - return "GstCapsFeatures.new_any()" - write = ["GstCapsFeatures("] - first = True - for feature in self.features: - if first: - first = False - else: - write.append(", ") - write.append(repr(feature)) - write.append(")") - return "".join(write) - - def __str__(self): - """Emulate gst_caps_features_to_string""" - if not self.features and self.is_any: - return "ANY" - write = [] - first = True - for feature in self.features: - feature = unicode_to_str(feature) - if type(feature) is not str: - raise TypeError( - "Found a feature that is not a str type") - if first: - first = False - else: - write.append(", ") - write.append(feature) - return "".join(write) - - -@otio.core.register_type -class GstCaps(otio.core.SerializableObject): - """ - An OpenTimelineIO Schema that acts as an ordered collection of - GstStructures, essentially mimicking the GstCaps of the Gstreamer C - libarary. Each GstStructure is linked to a GstCapsFeatures, which is - a list of features. - - In particular, this schema mimics the gst_caps_to_string and - gst_caps_from_string C methods. - """ - _serializable_label = "GstCaps.1" - - structs = otio.core.serializable_field( - "structs", list, "A list of GstStructures and GstCapsFeatures, " - "of the form:\n" - " (struct, features)\n" - "where 'struct' is a GstStructure, and 'features' is a " - "GstCapsFeatures") - flags = otio.core.serializable_field( - "flags", int, "Additional GstCapsFlags on the GstCaps") - - GST_CAPS_FLAG_ANY = 1 << 4 - # from GST_MINI_OBJECT_FLAG_LAST - - def __init__(self, *structs): - """ - Initialize the GstCaps. - - 'structs' should be a series of GstStructures, and - GstCapsFeatures pairs: - struct0, features0, struct1, features1, ... - None may be given in place of a GstCapsFeatures, in which case - an empty features is assigned to the structure. - - Note, this instance will need to take ownership of any given - GstStructure or GstCapsFeatures. - """ - otio.core.SerializableObject.__init__(self) - if len(structs) % 2: - raise InvalidValueError( - "*structs", structs, "an even number of arguments") - self.flags = 0 - self.structs = [] - struct = None - for index, arg in enumerate(structs): - if index % 2 == 0: - struct = arg - else: - self.append(struct, arg) - - def get_structure(self, index): - """Return the GstStructure at the given index""" - return self.structs[index][0] - - def get_features(self, index): - """Return the GstStructure at the given index""" - return self.structs[index][1] - - def __getitem__(self, index): - return self.get_structure(index) - - def __len__(self): - return len(self.structs) - - @classmethod - def new_any(cls): - caps = cls() - caps.flags = cls.GST_CAPS_FLAG_ANY - return caps - - def is_any(self): - return self.flags & self.GST_CAPS_FLAG_ANY != 0 - - FEATURES_FORMAT = r"\((?P[^)]*)\)" - NAME_FEATURES_REGEX = re.compile( - GstStructure.ASCII_SPACES + GstStructure.NAME_FORMAT - + r"(" + FEATURES_FORMAT + r")?" + GstStructure.END_FORMAT) - - @classmethod - def new_from_str(cls, read): - """ - Returns a new instance of GstCaps, based on the Gst library - function gst_caps_from_string. - Strings obtained from the GstCaps str() method can be parsed in - to recreate the original GstCaps. - """ - read = unicode_to_str(read) - if type(read) is not str: - _wrong_type_for_arg(read, "str", "read") - if read == "ANY": - return cls.new_any() - if read in ("EMPTY", "NONE"): - return cls() - structs = [] - # restriction-caps is otherwise serialized in the format: - # "struct-name-nums(feature), " - # "field1=(type1)val1, field2=(type2)val2; " - # "struct-name-alphas(feature), " - # "fieldA=(typeA)valA, fieldB=(typeB)valB" - # Note the lack of ';' for the last structure, and the - # '(feature)' is optional. - # - # NOTE: gst_caps_from_string also accepts: - # "struct-name(feature" - # without the final ')', but this must be the end of the string, - # but we will require that this final ')' is still given - while read: - match = cls.NAME_FEATURES_REGEX.match(read) - if match is None: - raise DeserializeError( - read, "does not match the regular expression {}" - "".format(cls.NAME_FEATURE_REGEX.pattern)) - read = read[match.end("end"):] - name = match.group("name") - features = match.group("features") - # NOTE: features may be None since the features part of the - # regular expression is optional - if features is None: - features = GstCapsFeatures() - else: - features = GstCapsFeatures.new_from_str(features) - fields, read = GstStructure._parse_fields(read) - structs.append(GstStructure(name, fields)) - structs.append(features) - return cls(*structs) - - def __repr__(self): - if self.is_any(): - return "GstCaps.new_any()" - write = ["GstCaps("] - first = True - for struct in self.structs: - if first: - first = False - else: - write.append(", ") - write.append(repr(struct[0])) - write.append(", ") - write.append(repr(struct[1])) - write.append(")") - return "".join(write) - - def __str__(self): - """Emulate gst_caps_to_string""" - if self.is_any(): - return "ANY" - if not self.structs: - return "EMPTY" - first = True - write = [] - for struct, features in self.structs: - if first: - first = False - else: - write.append("; ") - write.append(struct._name_to_str()) - if features.is_any or features.features: - # NOTE: is gst_caps_to_string, the feature will not - # be written if it only contains the - # GST_FEATURE_MEMORY_SYSTEM_MEMORY feature, since this - # considered equal to being an empty features. - # We do not seem to require this behaviour - write.append(f"({features!s})") - write.append(struct._fields_to_str()) - return "".join(write) - - def append(self, structure, features=None): - """Append a structure with the given features""" - if not isinstance(structure, GstStructure): - _wrong_type_for_arg(structure, "GstStructure", "structure") - if features is None: - features = GstCapsFeatures() - if not isinstance(features, GstCapsFeatures): - _wrong_type_for_arg( - features, "GstCapsFeatures or None", "features") - self.structs.append((structure, features)) - - -@otio.core.register_type -class GESMarker(otio.core.SerializableObject): - """ - An OpenTimelineIO Schema that is a timestamp with metadata, - essentially mimicking the GstMarker of the GES C libarary. - """ - _serializable_label = "GESMarker.1" - - position = otio.core.serializable_field( - "position", int, "The timestamp of the marker as a " - "GstClockTime (unsigned integer time in nanoseconds)") - - metadatas = otio.core.serializable_field( - "metadatas", GstStructure, "The metadatas associated with the " - "position") - - def __init__(self, position=0, metadatas=None): - """ - Note, this instance will need to take ownership of any given - GstSructure. - """ - otio.core.SerializableObject.__init__(self) - if metadatas is None: - metadatas = GstStructure("metadatas") - if type(position) is not int: - # TODO: remove below once python2 has ended - # currently in python2, can receive either an int or - # a long - if isinstance(position, numbers.Integral): - position = int(position) - # may still be an int if the position is too big - if type(position) is not int: - _wrong_type_for_arg(position, "int", "position") - if position < 0: - raise InvalidValueError( - "position", position, "a positive integer") - - if not isinstance(metadatas, GstStructure): - _wrong_type_for_arg(metadatas, "GstStructure", "metadatas") - _force_gst_structure_name(metadatas, "metadatas", "GESMarker") - self.position = position - self.metadatas = metadatas - - GES_META_MARKER_COLOR = "marker-color" - - def set_color_from_argb(self, argb): - """Set the color of the marker using the AARRGGBB hex value""" - if not isinstance(argb, int): - _wrong_type_for_arg(argb, "int", "argb") - if argb < 0 or argb > 0xffffffff: - raise InvalidValueError( - "argb", argb, "an unsigned 8 digit AARRGGBB hexadecimal") - self.metadatas.set(self.GES_META_MARKER_COLOR, "uint", argb) - - def is_colored(self): - """Return whether a marker is colored""" - return self.GES_META_MARKER_COLOR in self.metadatas.fields - - def get_argb_color(self): - """Return the markers color, or None if it has not been set""" - if self.is_colored: - return self.metadatas[self.GES_META_MARKER_COLOR] - return None - - OTIO_COLOR_TO_ARGB = { - otio.schema.MarkerColor.RED: 0xffff0000, - otio.schema.MarkerColor.PINK: 0xffff7070, - otio.schema.MarkerColor.ORANGE: 0xffffa000, - otio.schema.MarkerColor.YELLOW: 0xffffff00, - otio.schema.MarkerColor.GREEN: 0xff00ff00, - otio.schema.MarkerColor.CYAN: 0xff00ffff, - otio.schema.MarkerColor.BLUE: 0xff0000ff, - otio.schema.MarkerColor.PURPLE: 0xffa000d0, - otio.schema.MarkerColor.MAGENTA: 0xffff00ff, - otio.schema.MarkerColor.WHITE: 0xffffffff, - otio.schema.MarkerColor.BLACK: 0xff000000 - } - - def set_color_from_otio_color(self, otio_color): - """ - Set the color of the marker using to an otio color, by mapping it - to a corresponding argb color. - """ - if otio_color not in self.OTIO_COLOR_TO_ARGB: - raise InvalidValueError( - "otio_color", otio_color, "an otio.schema.MarkerColor") - self.set_color_from_argb(self.OTIO_COLOR_TO_ARGB[otio_color]) - - @staticmethod - def _otio_color_from_hue(hue): - """Return an otio color, based on hue in [0.0, 1.0]""" - if hue <= 0.04 or hue > 0.93: - return otio.schema.MarkerColor.RED - if hue <= 0.13: - return otio.schema.MarkerColor.ORANGE - if hue <= 0.2: - return otio.schema.MarkerColor.YELLOW - if hue <= 0.43: - return otio.schema.MarkerColor.GREEN - if hue <= 0.52: - return otio.schema.MarkerColor.CYAN - if hue <= 0.74: - return otio.schema.MarkerColor.BLUE - if hue <= 0.82: - return otio.schema.MarkerColor.PURPLE - return otio.schema.MarkerColor.MAGENTA - - def get_nearest_otio_color(self): - """ - Return an otio.schema.MarkerColor based on the markers argb color, - or None if it has not been set. - For colors close to the otio color set, this should return the - expected color name. - For edge cases, the 'correct' color is more apparently subjective. - This method does not work well for colors that are fairly gray - (low saturation values in HLS). For really gray colours, WHITE or - BLACK will be returned depending on the lightness. - The transparency of a color is ignored. - """ - argb = self.get_argb_color() - if argb is None: - return None - nearest = None - red = float((argb & 0xff0000) >> 16) / 255.0 - green = float((argb & 0x00ff00) >> 8) / 255.0 - blue = float(argb & 0x0000ff) / 255.0 - hue, lightness, saturation = colorsys.rgb_to_hls(red, green, blue) - if saturation < 0.2: - if lightness > 0.65: - nearest = otio.schema.MarkerColor.WHITE - else: - nearest = otio.schema.MarkerColor.BLACK - if nearest is None: - if lightness < 0.13: - nearest = otio.schema.MarkerColor.BLACK - if lightness > 0.9: - nearest = otio.schema.MarkerColor.WHITE - if nearest is None: - nearest = self._otio_color_from_hue(hue) - if nearest == otio.schema.MarkerColor.RED \ - and lightness > 0.53: - nearest = otio.schema.MarkerColor.PINK - if nearest == otio.schema.MarkerColor.MAGENTA \ - and hue < 0.89 and lightness < 0.42: - # some darker magentas look more like purple - nearest = otio.schema.MarkerColor.PURPLE - return nearest - - def __repr__(self): - return "GESMarker({!r}, {!r})".format( - self.position, self.metadatas) - - -@otio.core.register_type -class GESMarkerList(otio.core.SerializableObject): - """ - An OpenTimelineIO Schema that is a list of GESMarkers, ordered by - their positions, essentially mimicking the GstMarkerList of the GES - C libarary. - """ - _serializable_label = "GESMarkerList.1" - - markers = otio.core.serializable_field( - "markers", list, "A list of GESMarkers") - - def __init__(self, *markers): - """ - Note, this instance will need to take ownership of any given - GESMarker. - """ - otio.core.SerializableObject.__init__(self) - self.markers = [] - for marker in markers: - self.add(marker) - - def add(self, marker): - """ - Add the GESMarker to the GESMarkerList such that the markers - list remains ordered by marker position (smallest first). - """ - if not isinstance(marker, GESMarker): - _wrong_type_for_arg(marker, "GESMarker", "marker") - for index, existing_marker in enumerate(self.markers): - if existing_marker.position > marker.position: - self.markers.insert(index, marker) - return - self.markers.append(marker) - - def markers_at_position(self, position): - """Return a list of markers with the given position""" - if not isinstance(position, int): - _wrong_type_for_arg(position, "int", "position") - return [mrk for mrk in self.markers if mrk.position == position] - - def __getitem__(self, index): - return self.markers[index] - - def __len__(self): - return len(self.markers) - - def __repr__(self): - write = ["GESMarkerList("] - first = True - for marker in self.markers: - if first: - first = False - else: - write.append(", ") - write.append(repr(marker)) - write.append(")") - return "".join(write) - - -@otio.core.register_type -class XgesTrack(otio.core.SerializableObject): - """ - An OpenTimelineIO Schema for storing a GESTrack. - - Not to be confused with OpenTimelineIO's schema.Track. - """ - _serializable_label = "XgesTrack.1" - - caps = otio.core.serializable_field( - "caps", GstCaps, "The GstCaps of the track") - track_type = otio.core.serializable_field( - "track-type", int, "The GESTrackType of the track") - properties = otio.core.serializable_field( - "properties", GstStructure, "The GObject properties of the track") - metadatas = otio.core.serializable_field( - "metadatas", GstStructure, "Metadata for the track") - - def __init__( - self, caps=None, track_type=GESTrackType.UNKNOWN, - properties=None, metadatas=None): - """ - Initialize the XgesTrack. - - properties and metadatas are passed as the second argument to - GstStructure. - """ - otio.core.SerializableObject.__init__(self) - if caps is None: - caps = GstCaps() - if not isinstance(caps, GstCaps): - _wrong_type_for_arg(caps, "GstCaps", "caps") - if not isinstance(track_type, int): - _wrong_type_for_arg(track_type, "int", "track_type") - if track_type not in GESTrackType.ALL_TYPES: - raise InvalidValueError( - "track_type", track_type, "a GESTrackType") - if properties is None: - properties = GstStructure("properties") - if metadatas is None: - metadatas = GstStructure("metadatas") - if not isinstance(properties, GstStructure): - _wrong_type_for_arg(properties, "GstStructure", "properties") - if not isinstance(metadatas, GstStructure): - _wrong_type_for_arg(metadatas, "GstStructure", "metadatas") - _force_gst_structure_name(properties, "properties", "XGESTrack") - _force_gst_structure_name(metadatas, "metadatas", "XGESTrack") - self.caps = caps - self.track_type = track_type - self.properties = properties - self.metadatas = metadatas - - def __repr__(self): - return \ - "XgesTrack(caps={!r}, track_type={!r}, "\ - "properties={!r}, metadatas={!r})".format( - self.caps, self.track_type, - self.properties, self.metadatas) - - @classmethod - def new_from_otio_track_kind(cls, kind): - """Return a new default XgesTrack for the given track kind""" - return cls.new_from_track_type(GESTrackType.from_otio_kind(kind)) - - @classmethod - def new_from_track_type(cls, track_type): - """Return a new default XgesTrack for the given track type""" - props = {} - if track_type == GESTrackType.VIDEO: - caps = GstCaps.new_from_str("video/x-raw(ANY)") - # TODO: remove restriction-caps property once the GES - # library supports default, non-NULL restriction-caps for - # GESVideoTrack (like GESAudioTrack). - # For time being, framerate is needed for stability. - props["restriction-caps"] = \ - ("string", "video/x-raw, framerate=(fraction)30/1") - elif track_type == GESTrackType.AUDIO: - caps = GstCaps.new_from_str("audio/x-raw(ANY)") - else: - raise UnhandledValueError("track_type", track_type) - props["mixing"] = ("boolean", True) - return cls(caps, track_type, GstStructure("properties", props)) diff --git a/docs/tutorials/adapters.md b/docs/tutorials/adapters.md index 39eed7d72..6cd0834d0 100644 --- a/docs/tutorials/adapters.md +++ b/docs/tutorials/adapters.md @@ -50,10 +50,6 @@ The contrib area hosts adapters which come from the community (_not_ supported - set `${OTIO_RV_PYTHON_LIB}` to point at the parent directory of `rvSession.py`: `setenv OTIO_RV_PYTHON_LIB /Applications/RV64.app/Contents/src/python` -## GStreamer Editing Services Adapter - -- Status: supported via the `xges` adapter. - ## Kdenlive Adapter - Status: supported via the kdenlive adapter diff --git a/docs/tutorials/otio-plugins.md b/docs/tutorials/otio-plugins.md index 3cc23f70f..fdc6e99e9 100644 --- a/docs/tutorials/otio-plugins.md +++ b/docs/tutorials/otio-plugins.md @@ -240,43 +240,6 @@ Adapter plugins convert to and from OpenTimelineIO. [Tutorial on how to write an adapter](write-an-adapter). -### xges - -``` -OpenTimelineIO GStreamer Editing Services XML Adapter. -``` - -*source*: `opentimelineio_contrib/adapters/xges.py` - - -*Supported Features (with arguments)*: - -- read_from_string: -``` -Necessary read method for otio adapter - - Args: - input_str (str): A GStreamer Editing Services formated project - - Returns: - OpenTimeline: An OpenTimeline object -``` - - input_str -- write_to_string: -``` -Necessary write method for otio adapter - - Args: - input_otio (OpenTimeline): An OpenTimeline object - - Returns: - str: The string contents of an FCP X XML -``` - - input_otio - - - - ## Media Linkers @@ -294,96 +257,6 @@ SchemaDef plugins define new external schema. [Tutorial on how to write a schemadef](write-a-schemadef). -### xges - -``` -OpenTimelineIO GStreamer Editing Services XML Adapter. -``` - -*source*: `opentimelineio_contrib/adapters/xges.py` - - -*Serializable Classes*: - -- GESMarker: -``` -An OpenTimelineIO Schema that is a timestamp with metadata, - essentially mimicking the GstMarker of the GES C libarary. -``` -- GESMarkerList: -``` -An OpenTimelineIO Schema that is a list of GESMarkers, - ordered by - their positions, essentially mimicking the GstMarkerList of the GES - C libarary. -``` -- GstCaps: -``` -An OpenTimelineIO Schema that acts as an ordered collection of - GstStructures, essentially mimicking the GstCaps of the Gstreamer C - libarary. Each GstStructure is linked to a GstCapsFeatures, which is - a list of features. - - In particular, this schema mimics the gst_caps_to_string and - gst_caps_from_string C methods. -``` -- GstCapsFeatures: -``` -An OpenTimelineIO Schema that contains a collection of - features, - mimicking a GstCapsFeatures of the Gstreamer C libarary. -``` -- GstStructure: -``` -An OpenTimelineIO Schema that acts as a named dictionary with - typed entries, essentially mimicking the GstStructure of the - GStreamer C library. - - In particular, this schema mimics the gst_structure_to_string and - gst_structure_from_string C methods. As such, it can be used to - read and write the properties and metadatas attributes found in - xges elements. - - Note that the types are to correspond to GStreamer/GES GTypes, - rather than python types. - - Current supported GTypes: - GType Associated Accepted - Python type aliases - ====================================== - gint int int, i - glong int - gint64 int - guint int uint, u - gulong int - guint64 int - gfloat float float, f - gdouble float double, d - gboolean bool boolean, - bool, b - string str or None str, s - GstFraction str or fraction - Fraction - GstStructure GstStructure structure - schema - GstCaps GstCaps - schema - GESMarkerList GESMarkerList - schema - - Note that other types can be given: these must be given as strings - and the user will be responsible for making sure they are already in - a serialized form. -``` -- XgesTrack: -``` -An OpenTimelineIO Schema for storing a GESTrack. - - Not to be confused with OpenTimelineIO's schema.Track. -``` - - - ## HookScripts