Skip to content

Commit 88ceece

Browse files
freesonluxojminor
authored andcommitted
AAF Writer: Improves support of nesting (#493)
When writing AAF Sequences, make sure each one is nested inside a Submaster OperationGroup.
1 parent b044e07 commit 88ceece

File tree

4 files changed

+93
-57
lines changed

4 files changed

+93
-57
lines changed

opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import uuid
3333
import opentimelineio as otio
3434
import os
35+
import copy
3536

3637

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

256257

258+
def _stackify_nested_groups(timeline):
259+
"""
260+
Ensure that all nesting in a given timeline is in a stack container.
261+
This conforms with how AAF thinks about nesting, there needs
262+
to be an outer container, even if it's just one object.
263+
"""
264+
copied = copy.deepcopy(timeline)
265+
for track in copied.tracks:
266+
for i, child in enumerate(track.each_child()):
267+
is_nested = isinstance(child, otio.schema.Track)
268+
is_parent_in_stack = isinstance(child.parent(), otio.schema.Stack)
269+
if is_nested and not is_parent_in_stack:
270+
stack = otio.schema.Stack()
271+
track.remove(child)
272+
stack.append(child)
273+
track.insert(i, stack)
274+
return copied
275+
276+
257277
class _TrackTranscriber(object):
258278
"""
259279
_TrackTranscriber is the base class for the conversion of a given otio track.
@@ -292,20 +312,11 @@ def transcribe(self, otio_child):
292312
source_clip = self.aaf_sourceclip(otio_child)
293313
return source_clip
294314
elif isinstance(otio_child, otio.schema.Track):
295-
operation_group = self.nesting_operation_group()
296-
sequence = operation_group.segments[0]
297-
length = 0
298-
for nested_otio_child in otio_child:
299-
result = self.transcribe(nested_otio_child)
300-
sequence.components.append(result)
301-
length += result.length
302-
303-
sequence.length = length
304-
operation_group.length = length
305-
return operation_group
315+
sequence = self.aaf_sequence(otio_child)
316+
return sequence
306317
elif isinstance(otio_child, otio.schema.Stack):
307-
raise otio.exceptions.NotSupportedError(
308-
"Unsupported otio child type: otio.schema.Stack")
318+
operation_group = self.aaf_operation_group(otio_child)
319+
return operation_group
309320
else:
310321
raise otio.exceptions.NotSupportedError(
311322
"Unsupported otio child type: {}".format(type(otio_child)))
@@ -367,7 +378,8 @@ def aaf_sourceclip(self, otio_clip):
367378
# We need both `start_time` and `duration`
368379
# Here `start` is the offset between `first` and `in` values.
369380

370-
offset = otio_clip.visible_range().start_time - otio_clip.available_range().start_time
381+
offset = (otio_clip.visible_range().start_time -
382+
otio_clip.available_range().start_time)
371383
start = offset.value
372384
length = otio_clip.visible_range().duration.value
373385

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

466+
def aaf_sequence(self, otio_track):
467+
"""Convert an otio Track into an aaf Sequence"""
468+
sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)
469+
length = 0
470+
for nested_otio_child in otio_track:
471+
result = self.transcribe(nested_otio_child)
472+
length += result.length
473+
sequence.components.append(result)
474+
sequence.length = length
475+
return sequence
476+
477+
def aaf_operation_group(self, otio_stack):
478+
"""
479+
Create and return an OperationGroup which will contain other AAF objects
480+
to support OTIO nesting
481+
"""
482+
# Create OperationDefinition
483+
op_def = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_SUBMASTER,
484+
"Submaster")
485+
self.aaf_file.dictionary.register_def(op_def)
486+
op_def.media_kind = self.media_kind
487+
datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind)
488+
489+
# These values are necessary for pyaaf2 OperationDefinitions
490+
op_def["IsTimeWarp"].value = False
491+
op_def["Bypass"].value = 0
492+
op_def["NumberInputs"].value = -1
493+
op_def["OperationCategory"].value = "OperationCategory_Effect"
494+
op_def["DataDefinition"].value = datadef
495+
496+
# Create OperationGroup
497+
operation_group = self.aaf_file.create.OperationGroup(op_def)
498+
operation_group.media_kind = self.media_kind
499+
operation_group["DataDefinition"].value = datadef
500+
501+
length = 0
502+
for nested_otio_child in otio_stack:
503+
result = self.transcribe(nested_otio_child)
504+
length += result.length
505+
operation_group.segments.append(result)
506+
operation_group.length = length
507+
return operation_group
508+
454509
def _create_tapemob(self, otio_clip):
455510
"""
456511
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):
512567
mastermob_slot.segment = mastermob_clip
513568
return mastermob, mastermob_slot
514569

515-
def nesting_operation_group(self):
516-
'''
517-
Create and return an OperationGroup which will contain other AAF objects
518-
to support OTIO nesting
519-
'''
520-
# Create OperationDefinition
521-
op_def = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_SUBMASTER,
522-
"Submaster")
523-
self.aaf_file.dictionary.register_def(op_def)
524-
op_def.media_kind = self.media_kind
525-
datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind)
526-
527-
# These values are necessary for pyaaf2 OperationDefinitions
528-
op_def["IsTimeWarp"].value = False
529-
op_def["Bypass"].value = 0
530-
op_def["NumberInputs"].value = -1
531-
op_def["OperationCategory"].value = "OperationCategory_Effect"
532-
op_def["DataDefinition"].value = datadef
533-
534-
# Create OperationGroup
535-
operation_group = self.aaf_file.create.OperationGroup(op_def)
536-
operation_group.media_kind = self.media_kind
537-
operation_group["DataDefinition"].value = datadef
538-
539-
# Sequence
540-
sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)
541-
operation_group.segments.append(sequence)
542-
return operation_group
543-
544570

545571
class VideoTrackTranscriber(_TrackTranscriber):
546572
"""Video track kind specialization of TrackTranscriber."""

opentimelineio_contrib/adapters/advanced_authoring_format.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -942,15 +942,17 @@ def read_from_file(filepath, simplify=True):
942942
def write_to_file(input_otio, filepath, **kwargs):
943943
with aaf2.open(filepath, "w") as f:
944944

945-
aaf_writer.validate_metadata(input_otio)
945+
timeline = aaf_writer._stackify_nested_groups(input_otio)
946946

947-
otio2aaf = aaf_writer.AAFFileTranscriber(input_otio, f, **kwargs)
947+
aaf_writer.validate_metadata(timeline)
948948

949-
if not isinstance(input_otio, otio.schema.Timeline):
949+
otio2aaf = aaf_writer.AAFFileTranscriber(timeline, f, **kwargs)
950+
951+
if not isinstance(timeline, otio.schema.Timeline):
950952
raise otio.exceptions.NotSupportedError(
951953
"Currently only supporting top level Timeline")
952954

953-
for otio_track in input_otio.tracks:
955+
for otio_track in timeline.tracks:
954956
# Ensure track must have clip to get the edit_rate
955957
if len(otio_track) == 0:
956958
continue
476 KB
Binary file not shown.

opentimelineio_contrib/adapters/tests/test_aaf_adapter.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@
5959
SAMPLE_DATA_DIR,
6060
"nesting_test.aaf"
6161
)
62+
NESTED_STACK_EXAMPLE_PATH = os.path.join(
63+
SAMPLE_DATA_DIR,
64+
"nested_stack.aaf"
65+
)
6266
NESTING_PREFLATTENED_EXAMPLE_PATH = os.path.join(
6367
SAMPLE_DATA_DIR,
6468
"nesting_test_preflattened.aaf"
@@ -875,6 +879,9 @@ def _verify_first_clip(self, original_timeline, aaf_path):
875879
def test_aaf_writer_nesting(self):
876880
self._verify_aaf(NESTING_EXAMPLE_PATH)
877881

882+
def test_aaf_writer_nested_stack(self):
883+
self._verify_aaf(NESTED_STACK_EXAMPLE_PATH)
884+
878885
def _verify_aaf(self, aaf_path):
879886
otio_timeline = otio.adapters.read_from_file(aaf_path, simplify=True)
880887
fd, tmp_aaf_path = tempfile.mkstemp(suffix='.aaf')
@@ -921,23 +928,24 @@ def _verify_aaf(self, aaf_path):
921928
otio_track.each_child(shallow_search=True),
922929
sequence.components):
923930
type_mapping = {
924-
aaf2.components.SourceClip: otio.schema.Clip,
925-
aaf2.components.Transition: otio.schema.Transition,
926-
aaf2.components.Filler: otio.schema.Gap,
927-
aaf2.components.OperationGroup: otio.schema.track.Track,
931+
otio.schema.Clip: aaf2.components.SourceClip,
932+
otio.schema.Transition: aaf2.components.Transition,
933+
otio.schema.Gap: aaf2.components.Filler,
934+
otio.schema.Stack: aaf2.components.OperationGroup,
935+
otio.schema.Track: aaf2.components.OperationGroup
928936
}
929-
self.assertEqual(type(otio_child),
930-
type_mapping[type(aaf_component)])
937+
self.assertEqual(type(aaf_component),
938+
type_mapping[type(otio_child)])
931939

932940
if isinstance(aaf_component, SourceClip):
933941
self._verify_compositionmob_sourceclip_structure(aaf_component)
934942

935943
if isinstance(aaf_component, aaf2.components.OperationGroup):
936-
aaf_nested_components = aaf_component.segments[0].components
937-
for nested_otio_child, aaf_nested_component in zip(
938-
otio_child.each_child(), aaf_nested_components):
944+
nested_aaf_segments = aaf_component.segments
945+
for nested_otio_child, nested_aaf_segment in zip(
946+
otio_child.each_child(), nested_aaf_segments):
939947
self._is_otio_aaf_same(nested_otio_child,
940-
aaf_nested_component)
948+
nested_aaf_segment)
941949
else:
942950
self._is_otio_aaf_same(otio_child, aaf_component)
943951

0 commit comments

Comments
 (0)