diff --git a/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py b/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py index cd7f8d8e0..dfd701a4f 100644 --- a/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py +++ b/opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py @@ -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") @@ -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. @@ -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))) @@ -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 @@ -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. @@ -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.""" diff --git a/opentimelineio_contrib/adapters/advanced_authoring_format.py b/opentimelineio_contrib/adapters/advanced_authoring_format.py index a9de14af2..bc8bb845e 100644 --- a/opentimelineio_contrib/adapters/advanced_authoring_format.py +++ b/opentimelineio_contrib/adapters/advanced_authoring_format.py @@ -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 diff --git a/opentimelineio_contrib/adapters/tests/sample_data/nested_stack.aaf b/opentimelineio_contrib/adapters/tests/sample_data/nested_stack.aaf new file mode 100755 index 000000000..76f317d48 Binary files /dev/null and b/opentimelineio_contrib/adapters/tests/sample_data/nested_stack.aaf differ diff --git a/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py b/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py index 1b5e50b08..ced90f9f4 100644 --- a/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py +++ b/opentimelineio_contrib/adapters/tests/test_aaf_adapter.py @@ -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" @@ -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') @@ -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)