Skip to content
Permalink
Browse files

Allow non-ASCII names in AAFs (#475)

* Allow for both str and unicode names.
* Fix otioview's frame all feature when you open a SerializableCollection.
* Adding test case for non-ASCII names and metadata in an AAF.
* Avoid leaking aaf file handles.
* Added a workaround for AAF SourceClips without a Mob.
* Added a test case for reading an AAF with multiple top level mobs.
* Rearranged the AAF tests into three classes: AAFReaderTests, AAFWriterTests and SimplifyTests.
  • Loading branch information...
jminor committed Apr 4, 2019
1 parent 5352c91 commit 41ecc67971dc3e074f27fbde89830d55bc7c6d1b
@@ -53,7 +53,7 @@ def __init__(
self._json_path = None
self._module = None

name = core.serializable_field("name", str, "Adapter name.")
name = core.serializable_field("name", doc="Adapter name.")
execution_scope = core.serializable_field(
"execution_scope",
str,
@@ -69,7 +69,7 @@ def __init__(
self.color = color
self.metadata = metadata or {}

name = core.serializable_field("name", str, "Name of this marker.")
name = core.serializable_field("name", doc="Name of this marker.")

marked_range = core.serializable_field(
"marked_range",
@@ -65,7 +65,6 @@ def __init__(

name = core.serializable_field(
"name",
str,
doc="SerializableCollection name."
)
_children = core.serializable_field(
@@ -217,10 +217,10 @@ def _from_aaf_file(clip):
mob_id = None
target_url = clip.media_reference.target_url
if os.path.isfile(target_url) and target_url.endswith("aaf"):
aaf_file = aaf2.open(clip.media_reference.target_url)
mastermobs = list(aaf_file.content.mastermobs())
if len(mastermobs) == 1:
mob_id = mastermobs[0].mob_id
with aaf2.open(clip.media_reference.target_url) as aaf_file:
mastermobs = list(aaf_file.content.mastermobs())
if len(mastermobs) == 1:
mob_id = mastermobs[0].mob_id
return mob_id

def _generate_empty_mobid(clip):
@@ -31,6 +31,7 @@
import os
import sys
import numbers
import copy
from collections import Iterable
import opentimelineio as otio

@@ -59,7 +60,7 @@ def _get_name(item):
if isinstance(item, aaf2.components.SourceClip):
try:
return item.mob.name or "Untitled SourceClip"
except RuntimeError:
except AttributeError:
# Some AAFs produce this error:
# RuntimeError: failed with [-2146303738]: mob not found
return "SourceClip Missing Mob?"
@@ -241,15 +242,16 @@ def _transcribe(item, parent, editRate, masterMobs):
# Evidently the last mob is the one with the timecode
mobs = _find_timecode_mobs(item)
# Get the Timecode start and length values
timecode_info = _extract_timecode_info(mobs[-1]) if mobs else None
last_mob = mobs[-1] if mobs else None
timecode_info = _extract_timecode_info(last_mob) if last_mob else None

startTime = int(metadata.get("StartTime", "0"))
if timecode_info:
timecode_start, timecode_length = timecode_info
startTime += timecode_start

# get the length of the clip in the composition
if masterMobs and str(item.mob.mob_id) in masterMobs:
if masterMobs and item.mob and str(item.mob.mob_id) in masterMobs:
length = item.length
result.source_range = otio.opentime.TimeRange(
otio.opentime.RationalTime(startTime, editRate),
@@ -458,10 +460,7 @@ def _transcribe(item, parent, editRate, masterMobs):

# If we didn't get a name yet, use the one we have in metadata
if result.name is None:
# TODO: Some AAFs contain non-utf8 names?
# This works in Python 2.7, but not 3.5:
# result.name = metadata["Name"].encode('utf8', 'replace')
result.name = str(metadata["Name"])
result.name = metadata["Name"]

# Attach the AAF metadata
if not result.metadata:
@@ -788,7 +787,10 @@ def _simplify(thing):
if thing.source_range:
# make sure it has a source_range first
if not result.source_range:
result.source_range = result.trimmed_range()
try:
result.source_range = result.trimmed_range()
except otio.exceptions.CannotComputeAvailableRangeError:
result.source_range = copy.copy(thing.source_range)
# modify the duration, but leave the start_time as is
result.source_range = otio.opentime.TimeRange(
result.source_range.start_time,
Binary file not shown.
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
# Copyright 2017 Pixar Animation Studios
#
@@ -106,6 +107,14 @@
SAMPLE_DATA_DIR,
"not_aaf.otio"
)
UTF8_CLIP_PATH = os.path.join(
SAMPLE_DATA_DIR,
"utf8.aaf"
)
MULTIPLE_TOP_LEVEL_MOBS_CLIP_PATH = os.path.join(
SAMPLE_DATA_DIR,
"multiple_top_level_mobs.aaf"
)


try:
@@ -130,7 +139,7 @@
not could_import_aaf,
"AAF module not found. You might need to set OTIO_AAF_PYTHON_LIB"
)
class AAFAdapterTest(unittest.TestCase):
class AAFReaderTests(unittest.TestCase):

def test_aaf_read(self):
aaf_path = SIMPLE_EXAMPLE_PATH
@@ -190,48 +199,6 @@ def test_aaf_read(self):
]
)

def test_aaf_simplify(self):
aaf_path = SIMPLE_EXAMPLE_PATH
timeline = otio.adapters.read_from_file(aaf_path, simplify=True)
self.assertIsNotNone(timeline)
self.assertEqual(type(timeline), otio.schema.Timeline)
self.assertEqual(timeline.name, "OTIO TEST 1.Exported.01")
fps = timeline.duration().rate
self.assertEqual(fps, 24.0)
self.assertEqual(
timeline.duration(),
otio.opentime.from_timecode("00:02:16:18", fps)
)
self.assertEqual(len(timeline.tracks), 3)
self.assertEqual(otio.schema.TrackKind.Video, timeline.tracks[0].kind)
self.assertEqual(otio.schema.TrackKind.Audio, timeline.tracks[1].kind)
self.assertEqual(otio.schema.TrackKind.Audio, timeline.tracks[2].kind)
for track in timeline.tracks:
self.assertNotEqual(type(track[0]), otio.schema.Track)
self.assertEqual(len(track), 5)

def test_aaf_no_simplify(self):
aaf_path = SIMPLE_EXAMPLE_PATH
collection = otio.adapters.read_from_file(aaf_path, simplify=False)
self.assertIsNotNone(collection)
self.assertEqual(type(collection), otio.schema.SerializableCollection)
self.assertEqual(len(collection), 1)

timeline = collection[0]
self.assertEqual(timeline.name, "OTIO TEST 1.Exported.01")
fps = timeline.duration().rate
self.assertEqual(fps, 24.0)
self.assertEqual(
timeline.duration(),
otio.opentime.from_timecode("00:02:16:18", fps)
)

self.assertEqual(len(timeline.tracks), 12)

video_track = timeline.tracks[8][0]
self.assertEqual(otio.schema.TrackKind.Video, video_track.kind)
self.assertEqual(len(video_track), 5)

def test_aaf_read_trims(self):
aaf_path = TRIMS_EXAMPLE_PATH
timeline = otio.adapters.read_from_file(aaf_path)
@@ -790,6 +757,38 @@ def test_essence_group(self):
timeline.duration()
)

def test_30fps(self):
tl = otio.adapters.read_from_file(FPS30_CLIP_PATH)
self.assertEqual(tl.duration().rate, 30)

def test_2997fps(self):
tl = otio.adapters.read_from_file(FPS2997_CLIP_PATH)
self.assertEqual(tl.duration().rate, 30000 / 1001.0)

def test_utf8_names(self):
timeline = otio.adapters.read_from_file(UTF8_CLIP_PATH)
self.assertEqual(
u"Sequence_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷.Exported.01",
timeline.name
)
video_track = timeline.video_tracks()[0]
first_clip = video_track[0]
self.assertEqual(
first_clip.name,
u"Clip_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷"
)
self.assertEqual(
first_clip.media_reference.metadata["AAF"]["UserComments"]["Comments"],
u"Comments_ABCXYZñç꜕∑´®†¥¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æΩ≈ç√∫˜µ≤≥÷"
)

def test_multiple_top_level_mobs(self):
result = otio.adapters.read_from_file(MULTIPLE_TOP_LEVEL_MOBS_CLIP_PATH)
self.assertIsInstance(result, otio.schema.SerializableCollection)
self.assertEqual(2, len(result))


class AAFWriterTests(unittest.TestCase):
def test_aaf_writer_simple(self):
self._verify_aaf(SIMPLE_EXAMPLE_PATH)

@@ -962,6 +961,48 @@ def _is_otio_aaf_same(self, otio_child, aaf_component):


class SimplifyTests(unittest.TestCase):
def test_aaf_simplify(self):
aaf_path = SIMPLE_EXAMPLE_PATH
timeline = otio.adapters.read_from_file(aaf_path, simplify=True)
self.assertIsNotNone(timeline)
self.assertEqual(type(timeline), otio.schema.Timeline)
self.assertEqual(timeline.name, "OTIO TEST 1.Exported.01")
fps = timeline.duration().rate
self.assertEqual(fps, 24.0)
self.assertEqual(
timeline.duration(),
otio.opentime.from_timecode("00:02:16:18", fps)
)
self.assertEqual(len(timeline.tracks), 3)
self.assertEqual(otio.schema.TrackKind.Video, timeline.tracks[0].kind)
self.assertEqual(otio.schema.TrackKind.Audio, timeline.tracks[1].kind)
self.assertEqual(otio.schema.TrackKind.Audio, timeline.tracks[2].kind)
for track in timeline.tracks:
self.assertNotEqual(type(track[0]), otio.schema.Track)
self.assertEqual(len(track), 5)

def test_aaf_no_simplify(self):
aaf_path = SIMPLE_EXAMPLE_PATH
collection = otio.adapters.read_from_file(aaf_path, simplify=False)
self.assertIsNotNone(collection)
self.assertEqual(type(collection), otio.schema.SerializableCollection)
self.assertEqual(len(collection), 1)

timeline = collection[0]
self.assertEqual(timeline.name, "OTIO TEST 1.Exported.01")
fps = timeline.duration().rate
self.assertEqual(fps, 24.0)
self.assertEqual(
timeline.duration(),
otio.opentime.from_timecode("00:02:16:18", fps)
)

self.assertEqual(len(timeline.tracks), 12)

video_track = timeline.tracks[8][0]
self.assertEqual(otio.schema.TrackKind.Video, video_track.kind)
self.assertEqual(len(video_track), 5)

def test_simplify_top_level_track(self):
"""Test for cases where a track has a single item but should not be
collapsed because it is the the last track in the stack ie:
@@ -1047,14 +1088,6 @@ def test_simplify_stack_track_clip(self):
for i in simple_tl.tracks:
self.assertNotEqual(type(i), otio.schema.Clip)

def test_30fps(self):
tl = otio.adapters.read_from_file(FPS30_CLIP_PATH)
self.assertEqual(tl.duration().rate, 30)

def test_2997fps(self):
tl = otio.adapters.read_from_file(FPS2997_CLIP_PATH)
self.assertEqual(tl.duration().rate, 30000 / 1001.0)


if __name__ == '__main__':
unittest.main()
@@ -1188,4 +1188,5 @@ def add_stack(self, stack):
self.frame_all()

def frame_all(self):
self.currentWidget().frame_all()
if self.currentWidget():
self.currentWidget().frame_all()

0 comments on commit 41ecc67

Please sign in to comment.
You can’t perform that action at this time.