Skip to content

Commit

Permalink
AAF Writer: Improves support of nesting (#493)
Browse files Browse the repository at this point in the history
When writing AAF Sequences, make sure each one is nested inside a Submaster OperationGroup.
  • Loading branch information
freesonluxo authored and jminor committed Apr 29, 2019
1 parent b044e07 commit 88ceece
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 57 deletions.
112 changes: 69 additions & 43 deletions opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py
Expand Up @@ -32,6 +32,7 @@
import uuid
import opentimelineio as otio
import os
import copy


AAF_PARAMETERDEF_PAN = aaf2.auid.AUID("e4962322-2267-11d3-8a4c-0050040ef7d2")
Expand Down Expand Up @@ -254,6 +255,25 @@ def _generate_empty_mobid(clip):
return clip_mob_ids


def _stackify_nested_groups(timeline):
"""
Ensure that all nesting in a given timeline is in a stack container.
This conforms with how AAF thinks about nesting, there needs
to be an outer container, even if it's just one object.
"""
copied = copy.deepcopy(timeline)
for track in copied.tracks:
for i, child in enumerate(track.each_child()):
is_nested = isinstance(child, otio.schema.Track)
is_parent_in_stack = isinstance(child.parent(), otio.schema.Stack)
if is_nested and not is_parent_in_stack:
stack = otio.schema.Stack()
track.remove(child)
stack.append(child)
track.insert(i, stack)
return copied


class _TrackTranscriber(object):
"""
_TrackTranscriber is the base class for the conversion of a given otio track.
Expand Down Expand Up @@ -292,20 +312,11 @@ def transcribe(self, otio_child):
source_clip = self.aaf_sourceclip(otio_child)
return source_clip
elif isinstance(otio_child, otio.schema.Track):
operation_group = self.nesting_operation_group()
sequence = operation_group.segments[0]
length = 0
for nested_otio_child in otio_child:
result = self.transcribe(nested_otio_child)
sequence.components.append(result)
length += result.length

sequence.length = length
operation_group.length = length
return operation_group
sequence = self.aaf_sequence(otio_child)
return sequence
elif isinstance(otio_child, otio.schema.Stack):
raise otio.exceptions.NotSupportedError(
"Unsupported otio child type: otio.schema.Stack")
operation_group = self.aaf_operation_group(otio_child)
return operation_group
else:
raise otio.exceptions.NotSupportedError(
"Unsupported otio child type: {}".format(type(otio_child)))
Expand Down Expand Up @@ -367,7 +378,8 @@ def aaf_sourceclip(self, otio_clip):
# We need both `start_time` and `duration`
# Here `start` is the offset between `first` and `in` values.

offset = otio_clip.visible_range().start_time - otio_clip.available_range().start_time
offset = (otio_clip.visible_range().start_time -
otio_clip.available_range().start_time)
start = offset.value
length = otio_clip.visible_range().duration.value

Expand Down Expand Up @@ -451,6 +463,49 @@ def aaf_transition(self, otio_transition):
transition["DataDefinition"].value = datadef
return transition

def aaf_sequence(self, otio_track):
"""Convert an otio Track into an aaf Sequence"""
sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)
length = 0
for nested_otio_child in otio_track:
result = self.transcribe(nested_otio_child)
length += result.length
sequence.components.append(result)
sequence.length = length
return sequence

def aaf_operation_group(self, otio_stack):
"""
Create and return an OperationGroup which will contain other AAF objects
to support OTIO nesting
"""
# Create OperationDefinition
op_def = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_SUBMASTER,
"Submaster")
self.aaf_file.dictionary.register_def(op_def)
op_def.media_kind = self.media_kind
datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind)

# These values are necessary for pyaaf2 OperationDefinitions
op_def["IsTimeWarp"].value = False
op_def["Bypass"].value = 0
op_def["NumberInputs"].value = -1
op_def["OperationCategory"].value = "OperationCategory_Effect"
op_def["DataDefinition"].value = datadef

# Create OperationGroup
operation_group = self.aaf_file.create.OperationGroup(op_def)
operation_group.media_kind = self.media_kind
operation_group["DataDefinition"].value = datadef

length = 0
for nested_otio_child in otio_stack:
result = self.transcribe(nested_otio_child)
length += result.length
operation_group.segments.append(result)
operation_group.length = length
return operation_group

def _create_tapemob(self, otio_clip):
"""
Return a physical sourcemob for an otio Clip based on the MobID.
Expand Down Expand Up @@ -512,35 +567,6 @@ def _create_mastermob(self, otio_clip, filemob, filemob_slot):
mastermob_slot.segment = mastermob_clip
return mastermob, mastermob_slot

def nesting_operation_group(self):
'''
Create and return an OperationGroup which will contain other AAF objects
to support OTIO nesting
'''
# Create OperationDefinition
op_def = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_SUBMASTER,
"Submaster")
self.aaf_file.dictionary.register_def(op_def)
op_def.media_kind = self.media_kind
datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind)

# These values are necessary for pyaaf2 OperationDefinitions
op_def["IsTimeWarp"].value = False
op_def["Bypass"].value = 0
op_def["NumberInputs"].value = -1
op_def["OperationCategory"].value = "OperationCategory_Effect"
op_def["DataDefinition"].value = datadef

# Create OperationGroup
operation_group = self.aaf_file.create.OperationGroup(op_def)
operation_group.media_kind = self.media_kind
operation_group["DataDefinition"].value = datadef

# Sequence
sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)
operation_group.segments.append(sequence)
return operation_group


class VideoTrackTranscriber(_TrackTranscriber):
"""Video track kind specialization of TrackTranscriber."""
Expand Down
10 changes: 6 additions & 4 deletions opentimelineio_contrib/adapters/advanced_authoring_format.py
Expand Up @@ -942,15 +942,17 @@ def read_from_file(filepath, simplify=True):
def write_to_file(input_otio, filepath, **kwargs):
with aaf2.open(filepath, "w") as f:

aaf_writer.validate_metadata(input_otio)
timeline = aaf_writer._stackify_nested_groups(input_otio)

otio2aaf = aaf_writer.AAFFileTranscriber(input_otio, f, **kwargs)
aaf_writer.validate_metadata(timeline)

if not isinstance(input_otio, otio.schema.Timeline):
otio2aaf = aaf_writer.AAFFileTranscriber(timeline, f, **kwargs)

if not isinstance(timeline, otio.schema.Timeline):
raise otio.exceptions.NotSupportedError(
"Currently only supporting top level Timeline")

for otio_track in input_otio.tracks:
for otio_track in timeline.tracks:
# Ensure track must have clip to get the edit_rate
if len(otio_track) == 0:
continue
Expand Down
Binary file not shown.
28 changes: 18 additions & 10 deletions opentimelineio_contrib/adapters/tests/test_aaf_adapter.py
Expand Up @@ -59,6 +59,10 @@
SAMPLE_DATA_DIR,
"nesting_test.aaf"
)
NESTED_STACK_EXAMPLE_PATH = os.path.join(
SAMPLE_DATA_DIR,
"nested_stack.aaf"
)
NESTING_PREFLATTENED_EXAMPLE_PATH = os.path.join(
SAMPLE_DATA_DIR,
"nesting_test_preflattened.aaf"
Expand Down Expand Up @@ -875,6 +879,9 @@ def _verify_first_clip(self, original_timeline, aaf_path):
def test_aaf_writer_nesting(self):
self._verify_aaf(NESTING_EXAMPLE_PATH)

def test_aaf_writer_nested_stack(self):
self._verify_aaf(NESTED_STACK_EXAMPLE_PATH)

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')
Expand Down Expand Up @@ -921,23 +928,24 @@ def _verify_aaf(self, aaf_path):
otio_track.each_child(shallow_search=True),
sequence.components):
type_mapping = {
aaf2.components.SourceClip: otio.schema.Clip,
aaf2.components.Transition: otio.schema.Transition,
aaf2.components.Filler: otio.schema.Gap,
aaf2.components.OperationGroup: otio.schema.track.Track,
otio.schema.Clip: aaf2.components.SourceClip,
otio.schema.Transition: aaf2.components.Transition,
otio.schema.Gap: aaf2.components.Filler,
otio.schema.Stack: aaf2.components.OperationGroup,
otio.schema.Track: aaf2.components.OperationGroup
}
self.assertEqual(type(otio_child),
type_mapping[type(aaf_component)])
self.assertEqual(type(aaf_component),
type_mapping[type(otio_child)])

if isinstance(aaf_component, SourceClip):
self._verify_compositionmob_sourceclip_structure(aaf_component)

if isinstance(aaf_component, aaf2.components.OperationGroup):
aaf_nested_components = aaf_component.segments[0].components
for nested_otio_child, aaf_nested_component in zip(
otio_child.each_child(), aaf_nested_components):
nested_aaf_segments = aaf_component.segments
for nested_otio_child, nested_aaf_segment in zip(
otio_child.each_child(), nested_aaf_segments):
self._is_otio_aaf_same(nested_otio_child,
aaf_nested_component)
nested_aaf_segment)
else:
self._is_otio_aaf_same(otio_child, aaf_component)

Expand Down

0 comments on commit 88ceece

Please sign in to comment.