From dd872a7db9207e378dae43ac65e87d019bf8c7c5 Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Tue, 22 Sep 2020 15:00:10 -0700 Subject: [PATCH 1/2] Adding a sample FCP 7 XML file with the following generators from premiere: Universal Counting Leader, Black Video, Color Matte, HD Bars and Tone, Transparent Video, Type Tool title. --- tests/sample_data/premiere_generators.xml | 929 ++++++++++++++++++++++ 1 file changed, 929 insertions(+) create mode 100644 tests/sample_data/premiere_generators.xml diff --git a/tests/sample_data/premiere_generators.xml b/tests/sample_data/premiere_generators.xml new file mode 100644 index 0000000000..20eeb0b713 --- /dev/null +++ b/tests/sample_data/premiere_generators.xml @@ -0,0 +1,929 @@ + + + + + ce4dd6c6-f46e-4ec6-9191-c0da728298b2 + 856 + + 24 + TRUE + + Generators + + + + + + + 24 + TRUE + + 00:00:00:00 + 0 + NDF + + + + + + + + + + + + From 032668da88f16732b5a66d6d7aebe4c5ec7b5bd8 Mon Sep 17 00:00:00 2001 From: Eric Reinecke Date: Tue, 22 Sep 2020 18:09:09 -0700 Subject: [PATCH 2/2] FCP 7 XML clipitem elements where a mediaSource is specified instead of a fileurl are now treated as generators. This represents the vast majority of Premiere Pro generators in FCP 7 XML. --- .../opentimelineio/adapters/fcp_xml.py | 96 +++++++++++++++---- tests/test_fcp7_xml_adapter.py | 35 ++++++- 2 files changed, 114 insertions(+), 17 deletions(-) diff --git a/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py b/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py index d99c24e211..d6d098edb1 100644 --- a/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py +++ b/src/py-opentimelineio/opentimelineio/adapters/fcp_xml.py @@ -854,7 +854,7 @@ def media_reference_for_file_element(self, file_element, context): name = _name_from_element(file_element) # Get the full metadata - metadata_ignore_keys = {"duration", "name", "pathurl"} + metadata_ignore_keys = {"duration", "name", "pathurl", "mediaSource"} md_dict = _xml_tree_to_dict(file_element, metadata_ignore_keys) metadata_dict = {META_NAMESPACE: md_dict} if md_dict else None @@ -865,6 +865,13 @@ def media_reference_for_file_element(self, file_element, context): else: path = None + # Determine the mediasource + mediasource_element = file_element.find("./mediaSource") + if mediasource_element is not None: + mediasource = mediasource_element.text + else: + mediasource = None + # Find the timing timecode_element = file_element.find("./timecode") if timecode_element is not None: @@ -887,19 +894,26 @@ def media_reference_for_file_element(self, file_element, context): else: available_range = None - if path is None: - media_reference = schema.MissingReference( + if path is not None: + media_reference = schema.ExternalReference( + target_url=path, + available_range=available_range, + metadata=metadata_dict, + ) + media_reference.name = name + elif mediasource is not None: + media_reference = schema.GeneratorReference( name=name, + generator_kind=mediasource, available_range=available_range, metadata=metadata_dict, ) else: - media_reference = schema.ExternalReference( - target_url=path, + media_reference = schema.MissingReference( + name=name, available_range=available_range, metadata=metadata_dict, ) - media_reference.name = name return media_reference @@ -912,10 +926,16 @@ def media_reference_for_effect_element(self, effect_element): :return: An :class: `schema.GeneratorReference` instance. """ name = _name_from_element(effect_element) - md_dict = _xml_tree_to_dict(effect_element, {"name"}) + md_dict = _xml_tree_to_dict(effect_element, {"name", "effectid"}) + + effectid_element = effect_element.find("./effectid") + generator_kind = ( + effectid_element.text if effectid_element is not None else "" + ) return schema.GeneratorReference( name=name, + generator_kind=generator_kind, metadata=({META_NAMESPACE: md_dict} if md_dict else None) ) @@ -1471,14 +1491,30 @@ def _build_file(media_reference, br_map): file_e = _element_with_item_metadata("file", media_reference) available_range = media_reference.available_range - url_path = _url_to_path(media_reference.target_url) - file_name = ( - media_reference.name if media_reference.name - else os.path.basename(url_path) + # If the media reference is of one of the supported types, populate + # the appropriate source info element + if isinstance(media_reference, schema.ExternalReference): + _append_new_sub_element( + file_e, 'pathurl', text=media_reference.target_url + ) + url_path = _url_to_path(media_reference.target_url) + + fallback_file_name = ( + media_reference.name if media_reference.name + else os.path.basename(url_path) + ) + elif isinstance(media_reference, schema.GeneratorReference): + _append_new_sub_element( + file_e, 'mediaSource', text=media_reference.generator_kind + ) + fallback_file_name = media_reference.generator_kind + + _append_new_sub_element( + file_e, + 'name', + text=(media_reference.name or fallback_file_name), ) - _append_new_sub_element(file_e, 'name', text=file_name) - _append_new_sub_element(file_e, 'pathurl', text=media_reference.target_url) # timing info file_e.append(_build_rate(available_range.start_time.rate)) @@ -1607,16 +1643,41 @@ def _build_clip_item_without_media( @_backreference_build("clipitem") def _build_clip_item(clip_item, timeline_range, transition_offsets, br_map): + # This is some wacky logic, but here's why: + # Pretty much any generator from Premiere just reports as being a clip that + # uses Slug as mediaSource rather than a pathurl (color matte seems to be + # the exception). I think this is becasue it is aiming to roundtrip effects + # with itself rather than try to make them backward compatable with FCP 7. + # This allows Premiere generators to still come in as slugs and still exist + # as placeholders for effects that may not have a true analog in FCP 7. + # Since OTIO does not yet interpret these generators into specific + # first-class schema objects (e.x. color matte, bars, etc.), the + # "artificial" mediaSources on clipitem and generatoritem both interpret as + # generator references. So, for the moment, to detect if likely have the + # metadata to make an fcp 7 style generatoritem we look for the effecttype + # field, if that is missing we write the generator using mediaSource in the + # Premiere Pro style. + # This adapter is currently built to effectively round-trip and let savvy + # users push the correct data into the metadata dictionary to drive + # behavior, but in the future when there are specific generator schema in + # otio we could correctly translate a first-class OTIO generator concept + # into an equivalent FCP 7 generatoritem or a Premiere Pro style overloaded + # clipitem. is_generator = isinstance( clip_item.media_reference, schema.GeneratorReference ) - tagname = "generatoritem" if is_generator else "clipitem" + media_ref_fcp_md = clip_item.media_reference.metadata.get('fcp_xml', {}) + is_generatoritem = ( + is_generator and 'effecttype' in media_ref_fcp_md + ) + + tagname = "generatoritem" if is_generatoritem else "clipitem" clip_item_e = _element_with_item_metadata(tagname, clip_item) if "frameBlend" not in clip_item_e.attrib: clip_item_e.attrib["frameBlend"] = "FALSE" - if is_generator: + if is_generatoritem: clip_item_e.append(_build_generator_effect(clip_item, br_map)) else: clip_item_e.append(_build_file(clip_item.media_reference, br_map)) @@ -1680,7 +1741,7 @@ def _build_generator_effect(clip_item, br_map): effect_element = _dict_to_xml_tree(fcp_xml_effect_info, "effect") # Validate the metadata and make sure it contains the required elements - for required in ("effectid", "effecttype", "mediatype", "effectcategory"): + for required in ("effecttype", "mediatype", "effectcategory"): if effect_element.find(required) is None: return _build_empty_file( generator_ref, @@ -1690,6 +1751,9 @@ def _build_generator_effect(clip_item, br_map): # Add the name _append_new_sub_element(effect_element, "name", text=generator_ref.name) + _append_new_sub_element( + effect_element, "effectid", text=generator_ref.generator_kind + ) return effect_element diff --git a/tests/test_fcp7_xml_adapter.py b/tests/test_fcp7_xml_adapter.py index 95502a9ca0..9ff56ab465 100644 --- a/tests/test_fcp7_xml_adapter.py +++ b/tests/test_fcp7_xml_adapter.py @@ -49,6 +49,9 @@ FILTER_JSON_EXAMPLE_PATH = os.path.join( SAMPLE_DATA_DIR, "premiere_example_filter.json" ) +GENERATOR_XML_EXAMPLE_PATH = os.path.join( + SAMPLE_DATA_DIR, "premiere_generators.xml" +) class TestFcp7XmlUtilities(unittest.TestCase, test_utils.OTIOAssertions): @@ -1104,9 +1107,9 @@ def test_roundtrip_mem2disk2mem(self): audio_reference.name = "test_wav_one" generator_reference = schema.GeneratorReference( name="Color", + generator_kind="Color", metadata={ "fcp_xml": { - "effectid": "Color", "effectcategory": "Matte", "effecttype": "generator", "mediatype": "video", @@ -1381,6 +1384,36 @@ def test_xml_with_empty_elements(self): self.assertEqual(len(timeline.video_tracks()), 12) self.assertEqual(len(timeline.video_tracks()[0]), 34) + def test_read_generators(self): + timeline = adapters.read_from_file(GENERATOR_XML_EXAMPLE_PATH) + + video_track = timeline.tracks[0] + audio_track = timeline.tracks[3] + self.assertEqual(len(video_track), 6) + self.assertEqual(len(audio_track), 3) + + # Check all video items are generators + self.assertTrue( + all( + isinstance(item.media_reference, schema.GeneratorReference) + for item in video_track + ) + ) + + # Check the video generator kinds + self.assertEqual( + [clip.media_reference.generator_kind for clip in video_track], + ["Slug", "Slug", "Color", "Slug", "Slug", "GraphicAndType"], + ) + + # Check all non-gap audio items are generators + self.assertTrue( + all( + isinstance(item.media_reference, schema.GeneratorReference) + for item in video_track if not isinstance(item, schema.Gap) + ) + ) + if __name__ == '__main__': unittest.main()