From 99b6b622d505b9e8025506eaa2089ee38f089dc0 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 27 Jun 2023 18:39:35 -0700 Subject: [PATCH] AAFWriter: added support for AAF user comments (#22) Signed-off-by: Tim Lehr --- .../adapters/aaf_adapter/aaf_writer.py | 25 ++++++++++++ tests/test_aaf_adapter.py | 40 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py index 63e6749..729eb69 100644 --- a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py +++ b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py @@ -5,6 +5,7 @@ Specifies how to transcribe an OpenTimelineIO file into an AAF file. """ +from numbers import Rational import aaf2 import abc @@ -87,6 +88,9 @@ def __init__(self, input_otio, aaf_file, **kwargs): self._unique_tapemobs = {} self._clip_mob_ids_map = _gather_clip_mob_ids(input_otio, **kwargs) + # transcribe timeline comments onto composition mob + self._transcribe_user_comments(input_otio, self.compositionmob) + def _unique_mastermob(self, otio_clip): """Get a unique mastermob, identified by clip metadata mob id.""" mob_id = self._clip_mob_ids_map.get(otio_clip) @@ -97,6 +101,14 @@ def _unique_mastermob(self, otio_clip): mastermob.mob_id = aaf2.mobid.MobID(mob_id) self.aaf_file.content.mobs.append(mastermob) self._unique_mastermobs[mob_id] = mastermob + + # transcribe clip comments onto master mob + self._transcribe_user_comments(otio_clip, mastermob) + + # transcribe media reference comments onto master mob. + # this might overwrite clip comments. + self._transcribe_user_comments(otio_clip.media_reference, mastermob) + return mastermob def _unique_tapemob(self, otio_clip): @@ -140,6 +152,19 @@ def track_transcriber(self, otio_track): f"Unsupported track kind: {otio_track.kind}") return transcriber + def _transcribe_user_comments(self, otio_item, target_mob): + """Transcribes user comments on `otio_item` onto `target_mob` in AAF.""" + + user_comments = otio_item.metadata.get("AAF", {}).get("UserComments", {}) + for key, val in user_comments.items(): + if isinstance(val, int): + target_mob.comments[key] = val + elif isinstance(val, (float, Rational)): + target_mob.comments[key] = aaf2.rational.AAFRational(val) + else: + # ensure we can store comment value by converting it to unicode string + target_mob.comments[key] = str(val) + def validate_metadata(timeline): """Print a check of necessary metadata requirements for an otio timeline.""" diff --git a/tests/test_aaf_adapter.py b/tests/test_aaf_adapter.py index 9265199..40af097 100644 --- a/tests/test_aaf_adapter.py +++ b/tests/test_aaf_adapter.py @@ -1679,6 +1679,46 @@ def test_generator_reference(self): cl.media_reference.generator_kind = "not slug" otio.adapters.write_to_file(tl, tmp_aaf_path) + def test_aaf_writer_user_comments(self): + # construct simple timeline + timeline = otio.schema.Timeline() + range = otio.opentime.TimeRange( + otio.opentime.RationalTime(0, 24), + otio.opentime.RationalTime(100, 24), + ) + media_ref = otio.schema.ExternalReference(available_range=range) + clip = otio.schema.Clip(source_range=range) + clip.media_reference = media_ref + timeline.tracks.append(otio.schema.Track(children=[clip])) + + # add comments to clip + timeline + original_comments = { + "Test_String": "Test_Value", + "Test_Int": 1337, + "Test_Float": 13.37, + "Test_Bool": True, + } + + expected_comments = { + "Test_String": "Test_Value", + "Test_Int": "1337", + "Test_Float": aaf2.rational.AAFRational(13.37), + "Test_Bool": "True", + } + + timeline.metadata["AAF"] = {"UserComments": original_comments} + media_ref.metadata["AAF"] = {"UserComments": original_comments} + + _, tmp_aaf_path = tempfile.mkstemp(suffix='.aaf') + otio.adapters.write_to_file(timeline, tmp_aaf_path, use_empty_mob_ids=True) + + with aaf2.open(tmp_aaf_path) as aaf_file: + print(dict(next(aaf_file.content.mastermobs()).comments.items())) + master_mob = next(aaf_file.content.mastermobs()) + comp_mob = next(aaf_file.content.compositionmobs()) + self.assertEqual(dict(master_mob.comments.items()), expected_comments) + self.assertEqual(dict(comp_mob).comments.items(), expected_comments) + def _verify_aaf(self, aaf_path): otio_timeline = otio.adapters.read_from_file(aaf_path, simplify=True) fd, tmp_aaf_path = tempfile.mkstemp(suffix='.aaf')