Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AAFWriter: added support for AAF user comments (#22) #23

Merged
merged 1 commit into from
Mar 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py
Original file line number Diff line number Diff line change
@@ -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."""
44 changes: 44 additions & 0 deletions tests/test_aaf_adapter.py
Original file line number Diff line number Diff line change
@@ -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')