Skip to content

Commit a460aa7

Browse files
freesonluxossteinbach
authored andcommitted
AAF Writer: Improves support of nesting (AcademySoftwareFoundation#493)
When writing AAF Sequences, make sure each one is nested inside a Submaster OperationGroup.
1 parent 07803e0 commit a460aa7

File tree

4 files changed

+97
-65
lines changed

4 files changed

+97
-65
lines changed

contrib/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")
@@ -256,6 +257,25 @@ def _generate_empty_mobid(clip):
256257
return clip_mob_ids
257258

258259

260+
def _stackify_nested_groups(timeline):
261+
"""
262+
Ensure that all nesting in a given timeline is in a stack container.
263+
This conforms with how AAF thinks about nesting, there needs
264+
to be an outer container, even if it's just one object.
265+
"""
266+
copied = copy.deepcopy(timeline)
267+
for track in copied.tracks:
268+
for i, child in enumerate(track.each_child()):
269+
is_nested = isinstance(child, otio.schema.Track)
270+
is_parent_in_stack = isinstance(child.parent(), otio.schema.Stack)
271+
if is_nested and not is_parent_in_stack:
272+
stack = otio.schema.Stack()
273+
track.remove(child)
274+
stack.append(child)
275+
track.insert(i, stack)
276+
return copied
277+
278+
259279
class _TrackTranscriber(object):
260280
"""
261281
_TrackTranscriber is the base class for the conversion of a given otio track.
@@ -294,20 +314,11 @@ def transcribe(self, otio_child):
294314
source_clip = self.aaf_sourceclip(otio_child)
295315
return source_clip
296316
elif isinstance(otio_child, otio.schema.Track):
297-
operation_group = self.nesting_operation_group()
298-
sequence = operation_group.segments[0]
299-
length = 0
300-
for nested_otio_child in otio_child:
301-
result = self.transcribe(nested_otio_child)
302-
sequence.components.append(result)
303-
length += result.length
304-
305-
sequence.length = int(length)
306-
operation_group.length = length
307-
return operation_group
317+
sequence = self.aaf_sequence(otio_child)
318+
return sequence
308319
elif isinstance(otio_child, otio.schema.Stack):
309-
raise otio.exceptions.NotSupportedError(
310-
"Unsupported otio child type: otio.schema.Stack")
320+
operation_group = self.aaf_operation_group(otio_child)
321+
return operation_group
311322
else:
312323
raise otio.exceptions.NotSupportedError(
313324
"Unsupported otio child type: {}".format(type(otio_child)))
@@ -369,7 +380,8 @@ def aaf_sourceclip(self, otio_clip):
369380
# We need both `start_time` and `duration`
370381
# Here `start` is the offset between `first` and `in` values.
371382

372-
offset = otio_clip.visible_range().start_time - otio_clip.available_range().start_time
383+
offset = (otio_clip.visible_range().start_time -
384+
otio_clip.available_range().start_time)
373385
start = offset.value
374386
length = otio_clip.visible_range().duration.value
375387

@@ -453,6 +465,49 @@ def aaf_transition(self, otio_transition):
453465
transition["DataDefinition"].value = datadef
454466
return transition
455467

468+
def aaf_sequence(self, otio_track):
469+
"""Convert an otio Track into an aaf Sequence"""
470+
sequence = self.aaf_file.create.Sequence(media_kind=self.media_kind)
471+
length = 0
472+
for nested_otio_child in otio_track:
473+
result = self.transcribe(nested_otio_child)
474+
length += result.length
475+
sequence.components.append(result)
476+
sequence.length = length
477+
return sequence
478+
479+
def aaf_operation_group(self, otio_stack):
480+
"""
481+
Create and return an OperationGroup which will contain other AAF objects
482+
to support OTIO nesting
483+
"""
484+
# Create OperationDefinition
485+
op_def = self.aaf_file.create.OperationDef(AAF_OPERATIONDEF_SUBMASTER,
486+
"Submaster")
487+
self.aaf_file.dictionary.register_def(op_def)
488+
op_def.media_kind = self.media_kind
489+
datadef = self.aaf_file.dictionary.lookup_datadef(self.media_kind)
490+
491+
# These values are necessary for pyaaf2 OperationDefinitions
492+
op_def["IsTimeWarp"].value = False
493+
op_def["Bypass"].value = 0
494+
op_def["NumberInputs"].value = -1
495+
op_def["OperationCategory"].value = "OperationCategory_Effect"
496+
op_def["DataDefinition"].value = datadef
497+
498+
# Create OperationGroup
499+
operation_group = self.aaf_file.create.OperationGroup(op_def)
500+
operation_group.media_kind = self.media_kind
501+
operation_group["DataDefinition"].value = datadef
502+
503+
length = 0
504+
for nested_otio_child in otio_stack:
505+
result = self.transcribe(nested_otio_child)
506+
length += result.length
507+
operation_group.segments.append(result)
508+
operation_group.length = length
509+
return operation_group
510+
456511
def _create_tapemob(self, otio_clip):
457512
"""
458513
Return a physical sourcemob for an otio Clip based on the MobID.
@@ -514,35 +569,6 @@ def _create_mastermob(self, otio_clip, filemob, filemob_slot):
514569
mastermob_slot.segment = mastermob_clip
515570
return mastermob, mastermob_slot
516571

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

547573
class VideoTrackTranscriber(_TrackTranscriber):
548574
"""Video track kind specialization of TrackTranscriber."""

contrib/opentimelineio_contrib/adapters/advanced_authoring_format.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -943,21 +943,19 @@ def read_from_file(filepath, simplify=True):
943943

944944
def write_to_file(input_otio, filepath, **kwargs):
945945

946-
if not isinstance(input_otio, otio.schema.Timeline):
947-
raise otio.exceptions.NotSupportedError(
948-
"Currently only supporting top level Timeline"
949-
)
946+
with aaf2.open(filepath, "w") as f:
950947

951-
# in order to write a valid AAF file, OTIO may require either metadata
952-
# or access to the files at the ends of media references. This
953-
# preflight check makes sure that the conditions are correct for
954-
# authoring a valid AAF file.
955-
aaf_writer.validate_metadata(input_otio)
948+
timeline = aaf_writer._stackify_nested_groups(input_otio)
956949

957-
with aaf2.open(filepath, "w") as f:
958-
otio2aaf = aaf_writer.AAFFileTranscriber(input_otio, f, **kwargs)
950+
aaf_writer.validate_metadata(timeline)
951+
952+
otio2aaf = aaf_writer.AAFFileTranscriber(timeline, f, **kwargs)
953+
954+
if not isinstance(timeline, otio.schema.Timeline):
955+
raise otio.exceptions.NotSupportedError(
956+
"Currently only supporting top level Timeline")
959957

960-
for otio_track in input_otio.tracks:
958+
for otio_track in timeline.tracks:
961959
# Ensure track must have clip to get the edit_rate
962960
if len(otio_track) == 0:
963961
continue
476 KB
Binary file not shown.

contrib/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"
@@ -885,6 +889,9 @@ def _verify_first_clip(self, original_timeline, aaf_path):
885889
def test_aaf_writer_nesting(self):
886890
self._verify_aaf(NESTING_EXAMPLE_PATH)
887891

892+
def test_aaf_writer_nested_stack(self):
893+
self._verify_aaf(NESTED_STACK_EXAMPLE_PATH)
894+
888895
def _verify_aaf(self, aaf_path):
889896
otio_timeline = otio.adapters.read_from_file(aaf_path, simplify=True)
890897
fd, tmp_aaf_path = tempfile.mkstemp(suffix='.aaf')
@@ -931,23 +938,24 @@ def _verify_aaf(self, aaf_path):
931938
otio_track.each_child(shallow_search=True),
932939
sequence.components):
933940
type_mapping = {
934-
aaf2.components.SourceClip: otio.schema.Clip,
935-
aaf2.components.Transition: otio.schema.Transition,
936-
aaf2.components.Filler: otio.schema.Gap,
937-
aaf2.components.OperationGroup: otio.schema.Track,
941+
otio.schema.Clip: aaf2.components.SourceClip,
942+
otio.schema.Transition: aaf2.components.Transition,
943+
otio.schema.Gap: aaf2.components.Filler,
944+
otio.schema.Stack: aaf2.components.OperationGroup,
945+
otio.schema.Track: aaf2.components.OperationGroup
938946
}
939-
self.assertEqual(type(otio_child),
940-
type_mapping[type(aaf_component)])
947+
self.assertEqual(type(aaf_component),
948+
type_mapping[type(otio_child)])
941949

942950
if isinstance(aaf_component, SourceClip):
943951
self._verify_compositionmob_sourceclip_structure(aaf_component)
944952

945953
if isinstance(aaf_component, aaf2.components.OperationGroup):
946-
aaf_nested_components = aaf_component.segments[0].components
947-
for nested_otio_child, aaf_nested_component in zip(
948-
otio_child.each_child(), aaf_nested_components):
954+
nested_aaf_segments = aaf_component.segments
955+
for nested_otio_child, nested_aaf_segment in zip(
956+
otio_child.each_child(), nested_aaf_segments):
949957
self._is_otio_aaf_same(nested_otio_child,
950-
aaf_nested_component)
958+
nested_aaf_segment)
951959
else:
952960
self._is_otio_aaf_same(otio_child, aaf_component)
953961

0 commit comments

Comments
 (0)