Skip to content

Commit

Permalink
AAF Writer: Making precheck() more robust (#512)
Browse files Browse the repository at this point in the history
* Adds better error messages for when the AAF Writer's precheck fails
* Adding precheck failer negative test
  • Loading branch information
freesonluxo authored and ssteinbach committed May 8, 2019
1 parent 929149e commit c35f6a1
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 63 deletions.
139 changes: 77 additions & 62 deletions opentimelineio_contrib/adapters/aaf_adapter/aaf_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import opentimelineio as otio
import os
import copy
import re


AAF_PARAMETERDEF_PAN = aaf2.auid.AUID("e4962322-2267-11d3-8a4c-0050040ef7d2")
Expand All @@ -52,6 +53,10 @@ class AAFAdapterError(otio.exceptions.OTIOError):
pass


class AAFValidationError(AAFAdapterError):
pass


class AAFFileTranscriber(object):
"""
AAFFileTranscriber
Expand Down Expand Up @@ -132,68 +137,40 @@ def track_transcriber(self, otio_track):
def validate_metadata(timeline):
"""Print a check of necessary metadata requirements for an otio timeline."""

errors = []

def _check(otio_child, keys_path):
keys = keys_path.split(".")
value = otio_child.metadata
try:
for key in keys:
value = value[key]
except KeyError:
print("{}({}) is missing required metadata {}".format(
otio_child.name, type(otio_child), keys_path))
errors.append((otio_child, keys_path))

def _check_equal(a, b, msg):
if a != b:
print(msg + ": {} vs. {}".format(a, b))
errors.append(msg)

# TODO: Check for existence of media_reference and media_reference.available_range

# Edit rate conformity
edit_rate = timeline.duration().rate
for otio_child in timeline.each_child():
msg = "{}({}) edit rate mismatch in ".format(otio_child.name, type(otio_child))
if isinstance(otio_child, otio.schema.Gap):
_check_equal(edit_rate,
otio_child.visible_range().duration.rate,
msg + "visible_range().duration.rate")
_check_equal(edit_rate,
otio_child.duration().rate,
msg + "duration().rate")
elif isinstance(otio_child, otio.schema.Clip):
_check_equal(edit_rate,
otio_child.visible_range().duration.rate,
msg + "visible_range().duration.rate")
_check_equal(edit_rate,
otio_child.duration().rate,
msg + "duration().rate")
_check_equal(edit_rate,
otio_child.media_reference.available_range.duration.rate,
msg + "media_reference.available_range.duration.rate")
_check_equal(edit_rate,
otio_child.media_reference.available_range.start_time.rate,
msg + "media_reference.available_range.start_time.rate")
elif isinstance(otio_child, otio.schema.Transition):
_check_equal(edit_rate,
otio_child.duration().rate,
msg + "duration().rate")

# Required metadata
for otio_child in timeline.each_child():
if isinstance(otio_child, otio.schema.Transition):
_check(otio_child, "AAF.PointList")
_check(otio_child, "AAF.OperationGroup")
_check(otio_child, "AAF.OperationGroup.Operation")
_check(otio_child,
"AAF.OperationGroup.Operation.DataDefinition.Name")
_check(otio_child, "AAF.OperationGroup.Operation.Description")
_check(otio_child, "AAF.OperationGroup.Operation.Name")
_check(otio_child, "AAF.CutPoint")

return errors
all_checks = [__check(timeline, "duration().rate")]
edit_rate = __check(timeline, "duration().rate").value

for child in timeline.each_child():
checks = []
if isinstance(child, otio.schema.Gap):
checks = [
__check(child, "duration().rate").equals(edit_rate)
]
if isinstance(child, otio.schema.Clip):
checks = [
__check(child, "duration().rate").equals(edit_rate),
__check(child, "media_reference.available_range.duration.rate"
).equals(edit_rate),
__check(child, "media_reference.available_range.start_time.rate"
).equals(edit_rate)
]
if isinstance(child, otio.schema.Transition):
checks = [
__check(child, "duration().rate").equals(edit_rate),
__check(child, "metadata['AAF']['PointList']"),
__check(child, "metadata['AAF']['OperationGroup']['Operation']"
"['DataDefinition']['Name']"),
__check(child, "metadata['AAF']['OperationGroup']['Operation']"
"['Description']"),
__check(child, "metadata['AAF']['OperationGroup']['Operation']"
"['Name']"),
__check(child, "metadata['AAF']['CutPoint']")
]
all_checks.extend(checks)

if any(check.errors for check in all_checks):
raise AAFValidationError("\n" + "\n".join(
sum([check.errors for check in all_checks], [])))


def _gather_clip_mob_ids(input_otio,
Expand Down Expand Up @@ -747,3 +724,41 @@ def _transition_parameters(self):
self.aaf_file.dictionary.lookup_parameterdef("ParameterDef_Level"))

return [param_def_level], level


class __check(object):
"""
__check is a private helper class that safely gets values given to check
for existence and equality
"""

def __init__(self, obj, tokenpath):
self.orig = obj
self.value = obj
self.errors = []
self.tokenpath = tokenpath
try:
for token in re.split(r"[\.\[]", tokenpath):
if token.endswith("()"):
self.value = getattr(self.value, token.replace("()", ""))()
elif "]" in token:
self.value = self.value[token.strip("[]'\"")]
else:
self.value = getattr(self.value, token)
except Exception as e:
self.value = None
self.errors.append("{}{} {}.{} does not exist, {}".format(
self.orig.name if hasattr(self.orig, "name") else "",
type(self.orig),
type(self.orig).__name__,
self.tokenpath, e))

def equals(self, val):
"""Check if the retrieved value is equal to a given value."""
if self.value is not None and self.value != val:
self.errors.append(
"{}{} {}.{} not equal to {} (expected) != {} (actual)".format(
self.orig.name if hasattr(self.orig, "name") else "",
type(self.orig),
type(self.orig).__name__, self.tokenpath, val, self.value))
return self
234 changes: 234 additions & 0 deletions opentimelineio_contrib/adapters/tests/sample_data/precheckfail.otio
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
{
"OTIO_SCHEMA": "Timeline.1",
"global_start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 86400
},
"metadata": {
"AAF": {
"ClassName": "CompositionMob",
"CreationTime": "2019-03-29 18:55:55",
"LastModified": "2019-03-29 18:55:14",
"MobAttributeList": {
"AudioPluginWindowTrack": 1,
"PRJ_BOUNDARY_FRAMES": 1,
"SEQUERNCE_FORMAT_STRING": "HD 1080p/24",
"SEQUERNCE_FORMAT_TYPE": 10,
"_IMAGE_BOUNDS_OVERRIDE": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?><Bounds> <Source>-800/1 -450/1 1600/1 900/1</Source></Bounds>",
"_USER_POS": 10,
"_VERSION": 2
},
"MobID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6a3b.ace913a2",
"Name": "OTIO_Test_ppjoshm1.Exported.01",
"Slots": {},
"UsageCode": "Usage_TopLevel"
}
},
"name": "OTIO_Test_ppjoshm1.Exported.01",
"tracks": {
"OTIO_SCHEMA": "Stack.1",
"children": [
{
"OTIO_SCHEMA": "Track.1",
"children": [
{
"OTIO_SCHEMA": "Clip.1",
"effects": [],
"markers": [],
"media_reference": {
"OTIO_SCHEMA": "MissingReference.1",
"available_range": null,
"metadata": {
"AAF": {
"ClassName": "MasterMob",
"ConvertFrameRate": false,
"CreationTime": "2019-03-29 18:52:18",
"LastModified": "2019-03-29 18:54:01",
"MobAttributeList": {
"_GEN": 1553885640,
"_IMPORTSETTING": "__AttributeList",
"_SAVED_AAF_AUDIO_LENGTH": 0,
"_SAVED_AAF_AUDIO_RATE_DEN": 1,
"_SAVED_AAF_AUDIO_RATE_NUM": 24,
"_USER_POS": 0,
"_VERSION": 2
},
"MobID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5",
"Name": "ppjoshm_1 (SIM1)",
"Slots": {}
}
},
"name": null
},
"metadata": {
"AAF": {
"ClassName": "SourceClip",
"ComponentAttributeList": {
"_IMAGE_BOUNDS_OVERRIDE": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?><Bounds> <Framing>-800/1 -450/1 1600/1 900/1</Framing> <Valid>-800/1 -450/1 1600/1 900/1</Valid> <Essence>-800/1 -450/1 1600/1 900/1</Essence> <Source>-800/1 -450/1 1600/1 900/1</Source></Bounds>"
},
"DataDefinition": {
"Description": "Picture Essence",
"Identification": "01030202-0100-0000-060e-2b3404010101",
"Name": "Picture"
},
"Length": 10,
"Name": "ppjoshm_1 (SIM1)",
"SourceID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5",
"SourceMobSlotID": 1,
"StartTime": 0
}
},
"name": "ppjoshm_1 (SIM1)",
"source_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 10
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 86501
}
}
}
],
"effects": [],
"kind": "Video",
"markers": [],
"metadata": {
"AAF": {
"ClassName": "TimelineMobSlot",
"EditRate": "24",
"MediaKind": "Picture",
"Name": "TimelineMobSlot",
"Origin": 0,
"PhysicalTrackNumber": 1,
"Segment": {
"Components": {},
"DataDefinition": {
"Description": "Picture Essence",
"Identification": "01030202-0100-0000-060e-2b3404010101",
"Name": "Picture"
},
"Length": 10
},
"SlotID": 9,
"SlotName": ""
}
},
"name": "TimelineMobSlot",
"source_range": null
},
{
"OTIO_SCHEMA": "Track.1",
"children": [
{
"OTIO_SCHEMA": "Clip.1",
"effects": [],
"markers": [],
"media_reference": {
"OTIO_SCHEMA": "MissingReference.1",
"available_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 48.0,
"value": 10
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 48.0,
"value": 0
}
},
"metadata": {
"AAF": {
"ClassName": "MasterMob",
"ConvertFrameRate": false,
"CreationTime": "2019-03-29 18:52:18",
"LastModified": "2019-03-29 18:54:01",
"MobAttributeList": {
"_GEN": 1553885640,
"_IMPORTSETTING": "__AttributeList",
"_SAVED_AAF_AUDIO_LENGTH": 0,
"_SAVED_AAF_AUDIO_RATE_DEN": 1,
"_SAVED_AAF_AUDIO_RATE_NUM": 24,
"_USER_POS": 0,
"_VERSION": 2
},
"MobID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5",
"Name": "ppjoshm_1 (SIM1)",
"Slots": {}
}
},
"name": null
},
"metadata": {
"AAF": {
"ClassName": "SourceClip",
"DataDefinition": {
"Description": "Sound Essence",
"Identification": "01030202-0200-0000-060e-2b3404010101",
"Name": "Sound"
},
"Length": 10,
"Name": "ppjoshm_1 (SIM1)",
"SourceID": "urn:smpte:umid:060a2b34.01010101.01010f00.13000000.060e2b34.7f7f2a80.5c9e6962.cd005cc5",
"SourceMobSlotID": 2,
"StartTime": 0
}
},
"name": "ppjoshm_1 (SIM1)",
"source_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 10
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 24.0,
"value": 0
}
}
}
],
"effects": [],
"kind": "Audio",
"markers": [],
"metadata": {
"AAF": {
"ClassName": "TimelineMobSlot",
"EditRate": "24",
"MediaKind": "Sound",
"Name": "TimelineMobSlot",
"Origin": 0,
"PhysicalTrackNumber": 1,
"Segment": {
"Components": {},
"DataDefinition": {
"Description": "Sound Essence",
"Identification": "01030202-0200-0000-060e-2b3404010101",
"Name": "Sound"
},
"Length": 10
},
"SlotID": 10,
"SlotName": ""
}
},
"name": "TimelineMobSlot",
"source_range": null
}
],
"effects": [],
"markers": [],
"metadata": {},
"name": "tracks",
"source_range": null
}
}
Loading

0 comments on commit c35f6a1

Please sign in to comment.