From 0a38b3ab8f22d919da1b4bae8c1b0226871d221f Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 22 Apr 2024 10:39:51 -0700 Subject: [PATCH 1/3] Writer: adding marker support to AAF writer Signed-off-by: Tim Lehr --- .../adapters/aaf_adapter/aaf_writer.py | 185 ++++++++++++++++-- 1 file changed, 174 insertions(+), 11 deletions(-) diff --git a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py index 45cd574..85bb4c7 100644 --- a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py +++ b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py @@ -6,8 +6,13 @@ Specifies how to transcribe an OpenTimelineIO file into an AAF file. """ from numbers import Rational +from pathlib import Path +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 @@ -16,6 +21,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") @@ -100,8 +107,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() @@ -362,8 +370,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 @@ -372,6 +381,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""" @@ -396,13 +408,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 """ @@ -413,8 +425,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 _create_timeline_mobslot(self): + def _aaf_physical_track_number(self) -> int: + pass + + @abc.abstractmethod + def _create_timeline_mobslot(self) \ + -> Tuple[aaf2.mobslots.TimelineMobSlot, aaf2.components.Sequence]: """ Return a timeline_mobslot and sequence for this track. @@ -427,11 +445,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): @@ -458,8 +477,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, @@ -543,6 +562,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) @@ -660,6 +807,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. @@ -750,6 +905,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") From 2d3c1f399f6421bc64cddcae469ee6dba3de7d8a Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 22 Apr 2024 18:41:23 -0700 Subject: [PATCH 2/3] Writer: Fixes error with edgecode when not using embedding Signed-off-by: Tim Lehr --- .../adapters/aaf_adapter/aaf_writer.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py index 85bb4c7..f4a5000 100644 --- a/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py +++ b/src/otio_aaf_adapter/adapters/aaf_adapter/aaf_writer.py @@ -6,7 +6,6 @@ Specifies how to transcribe an OpenTimelineIO file into an AAF file. """ from numbers import Rational -from pathlib import Path from typing import Tuple from typing import List from typing import Optional @@ -490,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): From 9fb8fcecf270d967644cda837728cfb3182cc988 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 22 Apr 2024 18:47:27 -0700 Subject: [PATCH 3/3] Reader: Fixed issue with markers not attaching due to Slot ID mismatch Signed-off-by: Tim Lehr --- .../adapters/advanced_authoring_format.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/otio_aaf_adapter/adapters/advanced_authoring_format.py b/src/otio_aaf_adapter/adapters/advanced_authoring_format.py index 4d9bfc6..81403aa 100644 --- a/src/otio_aaf_adapter/adapters/advanced_authoring_format.py +++ b/src/otio_aaf_adapter/adapters/advanced_authoring_format.py @@ -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 ) @@ -1248,7 +1248,6 @@ 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 @@ -1256,9 +1255,14 @@ def _attach_markers(collection): 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)