From d9c5c87afd62d10ad22b860c8c7ef2f553a43ac8 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 | 30 +++++++++++++ tests/test_aaf_adapter.py | 44 +++++++++++++++++++ 2 files changed, 74 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..520d38f 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 @@ -13,6 +14,7 @@ import os import copy import re +import logging AAF_PARAMETERDEF_PAN = aaf2.auid.AUID("e4962322-2267-11d3-8a4c-0050040ef7d2") @@ -27,6 +29,8 @@ AAF_VVAL_EXTRAPOLATION_ID = uuid.UUID("0e24dd54-66cd-4f1a-b0a0-670ac3a7a0b3") AAF_OPERATIONDEF_SUBMASTER = uuid.UUID("f1db0f3d-8d64-11d3-80df-006008143e6f") +logger = logging.getLogger(__name__) + def _is_considered_gap(thing): """Returns whether or not thiing can be considered gap. @@ -87,6 +91,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 +104,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 +155,21 @@ 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, str)): + target_mob.comments[key] = val + elif isinstance(val, (float, Rational)): + target_mob.comments[key] = aaf2.rational.AAFRational(val) + else: + logger.warning( + f"Skip transcribing unsupported comment value of type " + f"'{type(val)}' for key '{key}'." + ) + 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 73aed80..c1bcc72 100644 --- a/tests/test_aaf_adapter.py +++ b/tests/test_aaf_adapter.py @@ -1825,6 +1825,50 @@ 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_Unicode": "ラーメン", + "Test_Int": 1337, + "Test_Float": 13.37, + "Test_Bool": True, + "Test_Unsupported_List": ["test1", "test2", "test3"], + "Test_Unsupported_Dict": {"test_key": "test_value"}, + "Test_Unsupported_Schema": otio.schema.Marker(name="SomeMarker") + } + + expected_comments = { + "Test_String": "Test_Value", + "Test_Unicode": "ラーメン", + "Test_Int": 1337, + "Test_Float": aaf2.rational.AAFRational(13.37), + "Test_Bool": 1, + } + + 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: + 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')