Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Writer: add marker support #47

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 183 additions & 11 deletions src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
Specifies how to transcribe an OpenTimelineIO file into an AAF file.
"""
from numbers import Rational
from typing import Tuple
from typing import List
from typing import Optional

import aaf2
import aaf2.mobs
import abc
import uuid
import opentimelineio as otio
Expand All @@ -16,6 +20,8 @@
import re
import logging

import datetime
import getpass

AAF_PARAMETERDEF_PAN = aaf2.auid.AUID("e4962322-2267-11d3-8a4c-0050040ef7d2")
AAF_OPERATIONDEF_MONOAUDIOPAN = aaf2.auid.AUID("9d2ea893-0968-11d3-8a38-0050040ef7d2")
Expand Down Expand Up @@ -100,8 +106,9 @@ def __init__(self, input_otio, aaf_file, **kwargs):
AAFFileTranscriber requires an input timeline and an output pyaaf2 file handle.

Args:
input_otio: an input OpenTimelineIO timeline
aaf_file: a pyaaf2 file handle to an output file
input_otio(otio.schema.Timeline): an input OpenTimelineIO timeline
aaf_file(aaf2.file.AAFFile): a pyaaf2 file handle to an output file

"""
self.aaf_file = aaf_file
self.compositionmob = self.aaf_file.create.CompositionMob()
Expand Down Expand Up @@ -362,8 +369,9 @@ def __init__(self, root_file_transcriber, otio_track):
_TrackTranscriber

Args:
root_file_transcriber: the corresponding 'parent' AAFFileTranscriber object
otio_track: the given otio_track to convert
root_file_transcriber(AAFFileTranscriber): the corresponding 'parent'
AAFFileTranscriber object
otio_track(otio.schema.Track): the given otio_track to convert
"""
self.root_file_transcriber = root_file_transcriber
self.compositionmob = root_file_transcriber.compositionmob
Expand All @@ -372,6 +380,9 @@ def __init__(self, root_file_transcriber, otio_track):
self.edit_rate = self.otio_track.find_children()[0].duration().rate
self.timeline_mobslot, self.sequence = self._create_timeline_mobslot()
self.timeline_mobslot.name = self.otio_track.name
self.timeline_mobslot[
'PhysicalTrackNumber'
].value = self._aaf_physical_track_number

def transcribe(self, otio_child):
"""Transcribe otio child to corresponding AAF object"""
Expand All @@ -396,13 +407,13 @@ def transcribe(self, otio_child):

@property
@abc.abstractmethod
def media_kind(self):
def media_kind(self) -> str:
"""Return the string for what kind of track this is."""
pass

@property
@abc.abstractmethod
def _master_mob_slot_id(self):
def _master_mob_slot_id(self) -> int:
"""
Return the MasterMob Slot ID for the corresponding track media kind
"""
Expand All @@ -413,8 +424,14 @@ def _master_mob_slot_id(self):
# MasterMob slot 2. While this is a little inadequate, it works for now
pass

@property
@abc.abstractmethod
def _aaf_physical_track_number(self) -> int:
pass

@abc.abstractmethod
def _create_timeline_mobslot(self):
def _create_timeline_mobslot(self) \
-> Tuple[aaf2.mobslots.TimelineMobSlot, aaf2.components.Sequence]:
"""
Return a timeline_mobslot and sequence for this track.

Expand All @@ -427,11 +444,12 @@ def _create_timeline_mobslot(self):
pass

@abc.abstractmethod
def default_descriptor(self, otio_clip):
def default_descriptor(self, otio_clip) -> aaf2.essence.EssenceDescriptor:
pass

@abc.abstractmethod
def _transition_parameters(self):
def _transition_parameters(self) -> \
Tuple[List[aaf2.dictionary.ParameterDef], aaf2.misc.Parameter]:
pass

def aaf_network_locator(self, otio_external_ref):
Expand All @@ -458,8 +476,8 @@ def aaf_sourceclip(self, otio_clip):

offset = (otio_clip.visible_range().start_time -
otio_clip.available_range().start_time)
start = offset.value
length = otio_clip.visible_range().duration.value
start = int(offset.value)
length = int(otio_clip.visible_range().duration.value)

compmob_clip = self.compositionmob.create_source_clip(
slot_id=self.timeline_mobslot.slot_id,
Expand All @@ -471,6 +489,16 @@ def aaf_sourceclip(self, otio_clip):
compmob_clip.mob = mastermob
compmob_clip.slot = mastermob_slot
compmob_clip.slot_id = mastermob_slot.slot_id

# check if we need to set mark-in / mark-out
if otio_clip.visible_range() != otio_clip.available_range():
mastermob_slot["MarkIn"].value = int(
otio_clip.visible_range().start_time.value
)
mastermob_slot["MarkOut"].value = int(
otio_clip.visible_range().end_time_exclusive().value
)

return compmob_clip

def aaf_transition(self, otio_transition):
Expand Down Expand Up @@ -543,6 +571,134 @@ def aaf_transition(self, otio_transition):
transition["DataDefinition"].value = datadef
return transition

def _otio_marker_color_to_aaf_marker_color(self,
otio_color: Optional[
otio.schema.MarkerColor
]) -> dict:
color_map = {
otio.schema.MarkerColor.RED: {
"blue": 6564,
"green": 12134,
"red": 41471
},
otio.schema.MarkerColor.PINK: {
# not in MC, using red instead
"blue": 6564,
"green": 12134,
"red": 41471
},
otio.schema.MarkerColor.ORANGE: {
# not in MC, using red instead
"blue": 6564,
"green": 12134,
"red": 41471
},
otio.schema.MarkerColor.YELLOW: {
"blue": 6553,
"green": 58981,
"red": 58981
},
otio.schema.MarkerColor.GREEN: {
"blue": 13107,
"green": 52428,
"red": 13107
},
otio.schema.MarkerColor.CYAN: {
"blue": 52428,
"green": 52428,
"red": 13107
},
otio.schema.MarkerColor.BLUE: {
"blue": 52428,
"green": 13107,
"red": 13107
},
otio.schema.MarkerColor.PURPLE: {
# not in MC, using blue instead
"blue": 52428,
"green": 13107,
"red": 13107
},
otio.schema.MarkerColor.MAGENTA: {
"blue": 52428,
"green": 13107,
"red": 52428
},
otio.schema.MarkerColor.WHITE: {
"blue": 65535,
"green": 65535,
"red": 65534
},
otio.schema.MarkerColor.BLACK: {
"blue": 0,
"green": 0,
"red": 0
}
}
return color_map.get(otio_color, color_map[otio.schema.MarkerColor.RED])

def _transcribe_marker(self,
otio_marker: otio.schema.Marker,
otio_marker_parent: otio.core.Item,
aaf_sequence: aaf2.components.Sequence):
username = otio_marker.metadata.get(
"AAF", {}
).get("CommentMarkerUser", getpass.getuser())

color = otio_marker.metadata.get(
"AAF", {}
).get("CommentMarkerColor")
if not color:
color = self._otio_marker_color_to_aaf_marker_color(otio_marker.color)

transformed_range = otio_marker_parent.transformed_time_range(
otio_marker.marked_range, self.otio_track
)

marker = self.aaf_file.create.DescriptiveMarker()
# set all possible slots described by markers
# FIX: Markers can appear twice in the Avid marker list at the same time
marker['DescribedSlots'].value = {1, 2, 3, 4, 10, 11}
marker['Position'].value = int(transformed_range.start_time.value)
marker['Comment'].value = otio_marker.name
marker['CommentMarkerUser'].value = username
marker['CommentMarkerColor'].value = color

# FIX: The datetime doesn't correctly show in MC still
time_now = datetime.datetime.now()
marker['CommentMarkerTime'].value = time_now.strftime("%H:%M")
marker['CommentMarkerDate'].value = time_now.strftime("%m/%d/%Y")

aaf_sequence.components.append(marker)

def transcribe_aaf_descriptive_markers(self):
otio_markers_parents = {}
for otio_marker in self.otio_track.markers:
otio_markers_parents[otio_marker] = self.otio_track

for track_child in self.otio_track.find_children():
child_markers = getattr(track_child, "markers", [])
for child_marker in child_markers:
otio_markers_parents[child_marker] = track_child

if not otio_markers_parents:
return

event_mob_slot = self.aaf_file.create.EventMobSlot()
event_mob_slot['EditRate'].value = self.edit_rate
event_mob_slot['SlotID'].value = 1000
event_mob_slot[
'PhysicalTrackNumber'
].value = self._aaf_physical_track_number

sequence = self.aaf_file.create.Sequence("DescriptiveMetadata")

for otio_marker, marker_parent in otio_markers_parents.items():
self._transcribe_marker(otio_marker, marker_parent, sequence)

event_mob_slot.segment = sequence
self.compositionmob.slots.append(event_mob_slot)

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)
Expand Down Expand Up @@ -660,6 +816,14 @@ def media_kind(self):
def _master_mob_slot_id(self):
return 1

@property
def _aaf_physical_track_number(self) -> int:
video_tracks = []
for track in self.otio_track.parent():
if track.kind == otio.schema.TrackKind.Video:
video_tracks.append(track)
return video_tracks.index(self.otio_track) + 1

def _create_timeline_mobslot(self):
"""
Create a Sequence container (TimelineMobSlot) and Sequence.
Expand Down Expand Up @@ -750,6 +914,14 @@ def media_kind(self):
def _master_mob_slot_id(self):
return 2

@property
def _aaf_physical_track_number(self) -> int:
audio_tracks = []
for track in self.otio_track.parent():
if track.kind == otio.schema.TrackKind.Audio:
audio_tracks.append(track)
return audio_tracks.index(self.otio_track) + 1

def aaf_sourceclip(self, otio_clip):
# Parameter Definition
typedef = self.aaf_file.dictionary.lookup_typedef("Rational")
Expand Down
12 changes: 8 additions & 4 deletions src/otio_aaf_adapter/adapters/advanced_authoring_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ def _transcribe(item, parents, edit_rate, indent=0):

event_mob = event_mobs[-1]

metadata["AttachedSlotID"] = int(metadata["DescribedSlots"][0])
metadata["AttachedSlotIds"] = [int(x) for x in metadata["DescribedSlots"]]
metadata["AttachedPhysicalTrackNumber"] = int(
event_mob["PhysicalTrackNumber"].value
)
Expand Down Expand Up @@ -1248,17 +1248,21 @@ def _attach_markers(collection):
track_number = metadata.get("PhysicalTrackNumber")
if slot_id is None or track_number is None:
continue

tracks_map[(int(slot_id), int(track_number))] = track

# iterate all tracks for their markers and attach them to the matching item
for current_track in timeline.find_children(
descended_from_type=otio.schema.Track):
for marker in list(current_track.markers):
metadata = marker.metadata.get("AAF", {})
slot_id = metadata.get("AttachedSlotID")
attached_slot_ids = metadata.get("AttachedSlotIds", [])

track_number = metadata.get("AttachedPhysicalTrackNumber")
target_track = tracks_map.get((slot_id, track_number))
target_track = None
for slot_id in attached_slot_ids:
target_track = tracks_map.get((slot_id, track_number))
if target_track:
break

# remove marker from current parent track
current_track.markers.remove(marker)
Expand Down