diff --git a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json index 283eb1b157..58bb977191 100644 --- a/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json +++ b/contrib/opentimelineio_contrib/adapters/contrib_adapters.plugin_manifest.json @@ -1,12 +1,6 @@ { "OTIO_SCHEMA" : "PluginManifest.1", "adapters": [ - { - "OTIO_SCHEMA": "Adapter.1", - "name": "fcpx_xml", - "filepath": "fcpx_xml.py", - "suffixes": ["fcpxml"] - }, { "OTIO_SCHEMA": "Adapter.1", "name": "hls_playlist", diff --git a/contrib/opentimelineio_contrib/adapters/fcpx_xml.py b/contrib/opentimelineio_contrib/adapters/fcpx_xml.py index 20294334c1..e69de29bb2 100644 --- a/contrib/opentimelineio_contrib/adapters/fcpx_xml.py +++ b/contrib/opentimelineio_contrib/adapters/fcpx_xml.py @@ -1,1157 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright Contributors to the OpenTimelineIO project - -"""OpenTimelineIO Final Cut Pro X XML Adapter. """ -import os -import subprocess -from xml.etree import cElementTree -from xml.dom import minidom -from fractions import Fraction -from datetime import date -from urllib.parse import unquote - -import opentimelineio as otio - -META_NAMESPACE = "fcpx_xml" - -COMPOSABLE_ELEMENTS = ("video", "audio", "ref-clip", "asset-clip") - -FRAMERATE_FRAMEDURATION = {23.98: "1001/24000s", - 24: "25/600s", - 25: "1/25s", - 29.97: "1001/30000s", - 30: "100/3000s", - 50: "1/50s", - 59.94: "1001/60000s", - 60: "1/60s"} - - -def format_name(frame_rate, path): - """ - Helper to get the formatName used in FCP X XML format elements. This - uses ffprobe to get the frame size of the the clip at the provided path. - - Args: - frame_rate (int): The frame rate of the clip at the provided path - path (str): The path to the clip to probe - - Returns: - str: The format name. If empty, then ffprobe couldn't find the item - """ - - path = path.replace("file://", "") - path = unquote(path) - if not os.path.exists(path): - return "" - - try: - frame_size = subprocess.check_output( - [ - "ffprobe", - "-v", - "error", - "-select_streams", - "v:0", - "-show_entries", - "stream=height,width", - "-of", - "csv=s=x:p=0", - path - ] - ).decode("utf-8") - except (subprocess.CalledProcessError, OSError): - frame_size = "" - - if not frame_size: - return "" - - frame_size = frame_size.rstrip() - - if "1920" in frame_size: - frame_size = "1080" - - if frame_size.endswith("1280"): - frame_size = "720" - - return f"FFVideoFormat{frame_size}p{frame_rate}" - - -def to_rational_time(rational_number, fps): - """ - This converts a rational number value to an otio RationalTime object - - Args: - rational_number (str): This is a rational number from an FCP X XML - fps (int): The frame rate to use for calculating the rational time - - Returns: - RationalTime: A RationalTime object - """ - - if rational_number == "0s" or rational_number is None: - frames = 0 - else: - parts = rational_number.split("/") - if len(parts) > 1: - frames = int( - float(parts[0]) / float(parts[1].replace("s", "")) * float(fps) - ) - else: - frames = int(float(parts[0].replace("s", "")) * float(fps)) - - return otio.opentime.RationalTime(frames, int(fps)) - - -def from_rational_time(rational_time): - """ - This converts a RationalTime object to a rational number as a string - - Args: - rational_time (RationalTime): a rational time object - - Returns: - str: A rational number as a string - """ - - if int(rational_time.value) == 0: - return "0s" - result = Fraction( - float(rational_time.value) / float(rational_time.rate) - ).limit_denominator() - if str(result.denominator) == "1": - return f"{result.numerator}s" - return f"{result.numerator}/{result.denominator}s" - - -class FcpxOtio: - """ - This object is responsible for knowing how to convert an otio into an - FCP X XML - """ - - def __init__(self, otio_timeline): - self.otio_timeline = otio_timeline - self.fcpx_xml = cElementTree.Element("fcpxml", version="1.8") - self.resource_element = cElementTree.SubElement( - self.fcpx_xml, - "resources" - ) - if self.otio_timeline.schema_name() == "Timeline": - self.timelines = [self.otio_timeline] - else: - self.timelines = list( - self.otio_timeline.find_children( - descended_from_type=otio.schema.Timeline - ) - ) - - if len(self.timelines) > 1: - self.event_resource = cElementTree.SubElement( - self.fcpx_xml, - "event", - {"name": self._event_name()} - ) - else: - self.event_resource = self.fcpx_xml - - self.resource_count = 0 - - def to_xml(self): - """ - Convert an otio to an FCP X XML - - Returns: - str: FCPX XML content - """ - - for project in self.timelines: - top_sequence = self._stack_to_sequence(project.tracks) - - project_element = cElementTree.Element( - "project", - { - "name": project.name, - "uid": project.metadata.get("fcpx", {}).get("uid", "") - } - ) - project_element.append(top_sequence) - self.event_resource.append(project_element) - - if not self.timelines: - for clip in self._clips(): - if not clip.parent(): - self._add_asset(clip) - - for stack in self._stacks(): - ref_element = self._element_for_item( - stack, - None, - ref_only=True, - compound=True - ) - self.event_resource.append(ref_element) - child_parent_map = {c: p for p in self.fcpx_xml.iter() for c in p} - - for marker in [marker for marker in self.fcpx_xml.iter("marker")]: - parent = child_parent_map.get(marker) - marker_attribs = marker.attrib.copy() - parent.remove(marker) - cElementTree.SubElement( - parent, - "marker", - marker_attribs - ) - - xml = cElementTree.tostring( - self.fcpx_xml, - encoding="UTF-8", - method="xml" - ) - dom = minidom.parseString(xml) - pretty = dom.toprettyxml(indent=" ") - return pretty.replace( - '', - '\n\n' - ) - - def _stack_to_sequence(self, stack, compound_clip=False): - format_element = self._find_or_create_format_from(stack) - sequence_element = cElementTree.Element( - "sequence", - { - "duration": self._calculate_rational_number( - stack.duration().value, - stack.duration().rate - ), - "format": str(format_element.get("id")) - } - ) - spine = cElementTree.SubElement(sequence_element, "spine") - video_tracks = [ - t for t in stack - if t.kind == otio.schema.TrackKind.Video - ] - audio_tracks = [ - t for t in stack - if t.kind == otio.schema.TrackKind.Audio - ] - - for idx, track in enumerate(video_tracks): - self._track_for_spine(track, idx, spine, compound_clip) - - for idx, track in enumerate(audio_tracks): - lane_id = -(idx + 1) - self._track_for_spine(track, lane_id, spine, compound_clip) - return sequence_element - - def _track_for_spine(self, track, lane_id, spine, compound): - for child in self._lanable_items(track.find_children()): - if self._item_in_compound_clip(child) and not compound: - continue - child_element = self._element_for_item( - child, - lane_id, - compound=compound - ) - if not lane_id: - spine.append(child_element) - continue - if child.schema_name() == "Gap": - continue - - parent_element = self._find_parent_element( - spine, - track.trimmed_range_of_child(child).start_time, - self._find_or_create_format_from(track).get("id") - ) - offset = self._offset_based_on_parent( - child_element, - parent_element, - self._find_or_create_format_from(track).get("id") - ) - child_element.set( - "offset", - from_rational_time(offset) - ) - - parent_element.append(child_element) - return [] - - def _find_parent_element(self, spine, trimmed_range, format_id): - for item in spine.iter(): - if item.tag not in ("clip", "asset-clip", "gap", "ref-clip"): - continue - if item.get("lane") is not None: - continue - if item.tag == "gap" and item.find("./audio") is not None: - continue - offset = to_rational_time( - item.get("offset"), - self._frame_rate_from_element(item, format_id) - ) - duration = to_rational_time( - item.get("duration"), - self._frame_rate_from_element(item, format_id) - ) - total_time = offset + duration - if offset > trimmed_range: - continue - if total_time > trimmed_range: - return item - return None - - def _offset_based_on_parent(self, child, parent, default_format_id): - parent_offset = to_rational_time( - parent.get("offset"), - self._frame_rate_from_element(parent, default_format_id) - ) - child_offset = to_rational_time( - child.get("offset"), - self._frame_rate_from_element(child, default_format_id) - ) - - parent_start = to_rational_time( - parent.get("start"), - self._frame_rate_from_element(parent, default_format_id) - ) - return (child_offset - parent_offset) + parent_start - - def _frame_rate_from_element(self, element, default_format_id): - if element.tag == "gap": - format_id = default_format_id - - if element.tag == "ref-clip": - media_element = self._media_by_id(element.get("ref")) - asset = media_element.find("./sequence") - format_id = asset.get("format") - - if element.tag == "clip": - if element.find("./gap") is not None: - asset_id = element.find("./gap").find("./audio").get("ref") - else: - asset_id = element.find("./video").get("ref") - asset = self._asset_by_id(asset_id) - format_id = asset.get("format") - - if element.tag == "asset-clip": - asset = self._asset_by_id(element.get("ref")) - format_id = asset.get("format") - - format_element = self.resource_element.find( - f"./format[@id='{format_id}']" - ) - total, rate = format_element.get("frameDuration").split("/") - rate = rate.replace("s", "") - return int(float(rate) / float(total)) - - def _element_for_item(self, item, lane, ref_only=False, compound=False): - element = None - duration = self._calculate_rational_number( - item.duration().value, - item.duration().rate - ) - if item.schema_name() == "Clip": - asset_id = self._add_asset(item, compound_only=compound) - element = self._element_for_clip(item, asset_id, duration, lane) - - if item.schema_name() == "Gap": - element = self._element_for_gap(item, duration) - - if item.schema_name() == "Stack": - element = self._element_for_stack(item, duration, ref_only) - - if element is None: - return None - if lane: - element.set("lane", str(lane)) - for marker in item.markers: - marker_attribs = { - "start": from_rational_time(marker.marked_range.start_time), - "duration": from_rational_time(marker.marked_range.duration), - "value": marker.name - } - marker_element = cElementTree.Element( - "marker", - marker_attribs - ) - if marker.color == otio.schema.MarkerColor.RED: - marker_element.set("completed", "0") - if marker.color == otio.schema.MarkerColor.GREEN: - marker_element.set("completed", "1") - element.append(marker_element) - return element - - def _lanable_items(self, items): - return [ - item for item in items - if item.schema_name() in ["Gap", "Stack", "Clip"] - ] - - def _element_for_clip(self, item, asset_id, duration, lane): - element = cElementTree.Element( - "clip", - { - "name": item.name, - "offset": from_rational_time( - item.trimmed_range_in_parent().start_time - ), - "duration": duration - } - ) - start = from_rational_time(item.source_range.start_time) - if start != "0s": - element.set("start", str(start)) - if item.parent().kind == otio.schema.TrackKind.Video: - cElementTree.SubElement( - element, - "video", - { - "offset": "0s", - "ref": asset_id, - "duration": self._find_asset_duration(item) - } - ) - else: - gap_element = cElementTree.SubElement( - element, - "gap", - { - "name": "Gap", - "offset": "0s", - "duration": self._find_asset_duration(item) - } - ) - audio = cElementTree.SubElement( - gap_element, - "audio", - { - "offset": "0s", - "ref": asset_id, - "duration": self._find_asset_duration(item) - } - ) - if lane: - audio.set("lane", str(lane)) - return element - - def _element_for_gap(self, item, duration): - element = cElementTree.Element( - "gap", - { - "name": "Gap", - "duration": duration, - "offset": from_rational_time( - item.trimmed_range_in_parent().start_time - ), - "start": "3600s" - } - ) - return element - - def _element_for_stack(self, item, duration, ref_only): - media_element = self._add_compound_clip(item) - asset_id = media_element.get("id") - element = cElementTree.Element( - "ref-clip", - { - "name": item.name, - "duration": duration, - "ref": str(asset_id) - } - ) - if not ref_only: - element.set( - "offset", - from_rational_time( - item.trimmed_range_in_parent().start_time - ) - ) - element.set( - "start", - from_rational_time(item.source_range.start_time) - ) - if item.parent() and item.parent().kind == otio.schema.TrackKind.Audio: - element.set("srcEnable", "audio") - return element - - def _find_asset_duration(self, item): - if (item.media_reference and - not item.media_reference.is_missing_reference): - return self._calculate_rational_number( - item.media_reference.available_range.duration.value, - item.media_reference.available_range.duration.rate - ) - return self._calculate_rational_number( - item.duration().value, - item.duration().rate - ) - - def _find_asset_start(self, item): - if (item.media_reference and - not item.media_reference.is_missing_reference): - return self._calculate_rational_number( - item.media_reference.available_range.start_time.value, - item.media_reference.available_range.start_time.rate - ) - return self._calculate_rational_number( - item.source_range.start_time.value, - item.source_range.start_time.rate - ) - - def _clip_format_name(self, clip): - if clip.schema_name() in ("Stack", "Track"): - return "" - if not clip.media_reference: - return "" - - if clip.media_reference.is_missing_reference: - return "" - - return format_name( - clip.duration().rate, - clip.media_reference.target_url - ) - - def _find_or_create_format_from(self, clip): - frame_duration = self._framerate_to_frame_duration( - clip.duration().rate - ) - format_element = self._format_by_frame_rate(clip.duration().rate) - if format_element is None: - format_element = cElementTree.SubElement( - self.resource_element, - "format", - { - "id": self._resource_id_generator(), - "frameDuration": frame_duration, - "name": self._clip_format_name(clip) - } - ) - if format_element.get("name", "") == "": - format_element.set("name", self._clip_format_name(clip)) - return format_element - - def _add_asset(self, clip, compound_only=False): - format_element = self._find_or_create_format_from(clip) - asset = self._create_asset_element(clip, format_element) - - if not compound_only and self._asset_clip_by_name(clip.name) is None: - self._create_asset_clip_element( - clip, - format_element, - asset.get("id") - ) - - if not clip.parent(): - asset.set("hasAudio", "1") - asset.set("hasVideo", "1") - return asset.get("id") - if clip.parent().kind == otio.schema.TrackKind.Audio: - asset.set("hasAudio", "1") - if clip.parent().kind == otio.schema.TrackKind.Video: - asset.set("hasVideo", "1") - return asset.get("id") - - def _create_asset_clip_element(self, clip, format_element, resource_id): - duration = self._find_asset_duration(clip) - a_clip = cElementTree.SubElement( - self.event_resource, - "asset-clip", - { - "name": clip.name, - "format": format_element.get("id"), - "ref": resource_id, - "duration": duration - } - ) - if clip.media_reference and not clip.media_reference.is_missing_reference: - fcpx_metadata = clip.media_reference.metadata.get("fcpx", {}) - note_element = self._create_note_element( - fcpx_metadata.get("note", None) - ) - keyword_elements = self._create_keyword_elements( - fcpx_metadata.get("keywords", []) - ) - metadata_element = self._create_metadata_elements( - fcpx_metadata.get("metadata", None) - ) - - if note_element is not None: - a_clip.append(note_element) - if keyword_elements: - for keyword_element in keyword_elements: - a_clip.append(keyword_element) - if metadata_element is not None: - a_clip.append(metadata_element) - - def _create_asset_element(self, clip, format_element): - target_url = self._target_url_from_clip(clip) - asset = self._asset_by_path(target_url) - if asset is not None: - return asset - - asset = cElementTree.SubElement( - self.resource_element, - "asset", - { - "name": clip.name, - "src": target_url, - "format": format_element.get("id"), - "id": self._resource_id_generator(), - "duration": self._find_asset_duration(clip), - "start": self._find_asset_start(clip), - "hasAudio": "0", - "hasVideo": "0" - } - ) - return asset - - def _add_compound_clip(self, item): - media_element = self._media_by_name(item.name) - if media_element is not None: - return media_element - resource_id = self._resource_id_generator() - media_element = cElementTree.SubElement( - self.resource_element, - "media", - { - "name": self._compound_clip_name(item, resource_id), - "id": resource_id - } - ) - if item.metadata.get("fcpx", {}).get("uid", False): - media_element.set("uid", item.metadata.get("fcpx", {}).get("uid")) - media_element.append(self._stack_to_sequence(item, compound_clip=True)) - return media_element - - def _stacks(self): - return self.otio_timeline.find_children( - descended_from_type=otio.schema.Stack - ) - - def _clips(self): - return self.otio_timeline.find_children( - descended_from_type=otio.schema.Clip - ) - - def _resource_id_generator(self): - self.resource_count += 1 - return f"r{self.resource_count}" - - def _event_name(self): - if self.otio_timeline.name: - return self.otio_timeline.name - return date.strftime(date.today(), "%m-%e-%y") - - def _asset_by_path(self, path): - return self.resource_element.find(f"./asset[@src='{path}']") - - def _asset_by_id(self, asset_id): - return self.resource_element.find(f"./asset[@id='{asset_id}']") - - def _media_by_name(self, name): - return self.resource_element.find(f"./media[@name='{name}']") - - def _media_by_id(self, media_id): - return self.resource_element.find(f"./media[@id='{media_id}']") - - def _format_by_frame_rate(self, frame_rate): - frame_duration = self._framerate_to_frame_duration(frame_rate) - return self.resource_element.find( - f"./format[@frameDuration='{frame_duration}']" - ) - - def _asset_clip_by_name(self, name): - return self.event_resource.find( - f"./asset-clip[@name='{name}']" - ) - - # -------------------- - # static methods - # -------------------- - - @staticmethod - def _framerate_to_frame_duration(framerate): - frame_duration = FRAMERATE_FRAMEDURATION.get(int(framerate), "") - if not frame_duration: - frame_duration = FRAMERATE_FRAMEDURATION.get(float(framerate), "") - return frame_duration - - @staticmethod - def _target_url_from_clip(clip): - if (clip.media_reference and - not clip.media_reference.is_missing_reference): - return clip.media_reference.target_url - return f"file:///tmp/{clip.name}" - - @staticmethod - def _calculate_rational_number(duration, rate): - if int(duration) == 0: - return "0s" - result = Fraction(float(duration) / float(rate)).limit_denominator() - return f"{result.numerator}/{result.denominator}s" - - @staticmethod - def _compound_clip_name(compound_clip, resource_id): - if compound_clip.name: - return compound_clip.name - return f"compound_clip_{resource_id}" - - @staticmethod - def _item_in_compound_clip(item): - stack_count = 0 - parent = item.parent() - while parent is not None: - if parent.schema_name() == "Stack": - stack_count += 1 - parent = parent.parent() - return stack_count > 1 - - @staticmethod - def _create_metadata_elements(metadata): - if metadata is None: - return None - metadata_element = cElementTree.Element( - "metadata" - ) - for metadata_dict in metadata: - cElementTree.SubElement( - metadata_element, - "md", - { - "key": list(metadata_dict.keys())[0], - "value": list(metadata_dict.values())[0] - } - ) - return metadata_element - - @staticmethod - def _create_keyword_elements(keywords): - keyword_elements = [] - for keyword_dict in keywords: - keyword_elements.append( - cElementTree.Element( - "keyword", - dict(keyword_dict) - ) - ) - return keyword_elements - - @staticmethod - def _create_note_element(note): - if not note: - return None - note_element = cElementTree.Element( - "note" - ) - note_element.text = note - return note_element - - -class FcpxXml: - """ - This object is responsible for knowing how to convert an FCP X XML - otio into an otio timeline - """ - - def __init__(self, xml_string): - self.fcpx_xml = cElementTree.fromstring(xml_string) - self.child_parent_map = {c: p for p in self.fcpx_xml.iter() for c in p} - - def to_otio(self): - """ - Convert an FCP X XML to an otio - - Returns: - OpenTimeline: An OpenTimeline Timeline object - """ - - if self.fcpx_xml.find("./library") is not None: - return self._from_library() - if self.fcpx_xml.find("./event") is not None: - return self._from_event(self.fcpx_xml.find("./event")) - if self.fcpx_xml.find("./project") is not None: - return self._from_project(self.fcpx_xml.find("./project")) - if ((self.fcpx_xml.find("./asset-clip") is not None) or - (self.fcpx_xml.find("./ref-clip") is not None)): - return self._from_clips() - - def _from_library(self): - # We are just grabbing the first even in the project for now - return self._from_event(self.fcpx_xml.find("./library/event")) - - def _from_event(self, event_element): - container = otio.schema.SerializableCollection( - name=event_element.get("name") - ) - for project in event_element.findall("./project"): - container.append(self._from_project(project)) - return container - - def _from_project(self, project_element): - timeline = otio.schema.Timeline(name=project_element.get("name", "")) - timeline.tracks = self._squence_to_stack( - project_element.find("./sequence", {}) - ) - return timeline - - def _from_clips(self): - container = otio.schema.SerializableCollection() - if self.fcpx_xml.find("./asset-clip") is not None: - for asset_clip in self.fcpx_xml.findall("./asset-clip"): - container.append( - self._build_composable( - asset_clip, - asset_clip.get("format") - ) - ) - - if self.fcpx_xml.find("./ref-clip") is not None: - for ref_clip in self.fcpx_xml.findall("./ref-clip"): - container.append( - self._build_composable( - ref_clip, - "r1" - ) - ) - return container - - def _squence_to_stack(self, sequence_element, name="", source_range=None): - timeline_items = [] - lanes = [] - stack = otio.schema.Stack(name=name, source_range=source_range) - for element in sequence_element.iter(): - if element.tag not in COMPOSABLE_ELEMENTS: - continue - composable = self._build_composable( - element, - sequence_element.get("format") - ) - - offset, lane = self._offset_and_lane( - element, - sequence_element.get("format") - ) - - timeline_items.append( - { - "track": lane, - "offset": offset, - "composable": composable, - "audio_only": self._audio_only(element) - } - ) - - lanes.append(lane) - sorted_lanes = list(set(lanes)) - sorted_lanes.sort() - for lane in sorted_lanes: - sorted_items = self._sorted_items(lane, timeline_items) - track = otio.schema.Track( - name=lane, - kind=self._track_type(sorted_items) - ) - - for item in sorted_items: - frame_diff = ( - int(item["offset"].value) - track.duration().value - ) - if frame_diff > 0: - track.append( - self._create_gap( - 0, - frame_diff, - sequence_element.get("format") - ) - ) - track.append(item["composable"]) - stack.append(track) - return stack - - def _build_composable(self, element, default_format): - timing_clip = self._timing_clip(element) - source_range = self._time_range( - timing_clip, - self._format_id_for_clip(element, default_format) - ) - - if element.tag != "ref-clip": - otio_composable = otio.schema.Clip( - name=timing_clip.get("name"), - media_reference=self._reference_from_id( - element.get("ref"), - default_format - ), - source_range=source_range - ) - else: - media_element = self._compound_clip_by_id(element.get("ref")) - otio_composable = self._squence_to_stack( - media_element.find("./sequence"), - name=media_element.get("name"), - source_range=source_range - ) - - for marker in timing_clip.findall(".//marker"): - otio_composable.markers.append( - self._marker(marker, default_format) - ) - - return otio_composable - - def _marker(self, element, default_format): - if element.get("completed", None) and element.get("completed") == "1": - color = otio.schema.MarkerColor.GREEN - if element.get("completed", None) and element.get("completed") == "0": - color = otio.schema.MarkerColor.RED - if not element.get("completed", None): - color = otio.schema.MarkerColor.PURPLE - - otio_marker = otio.schema.Marker( - name=element.get("value", ""), - marked_range=self._time_range(element, default_format), - color=color - ) - return otio_marker - - def _audio_only(self, element): - if element.tag == "audio": - return True - if element.tag == "asset-clip": - asset = self._asset_by_id(element.get("ref", None)) - if asset is not None and asset.get("hasVideo", "0") == "0": - return True - if element.tag == "ref-clip": - if element.get("srcEnable", "video") == "audio": - return True - return False - - def _create_gap(self, start_frame, number_of_frames, defualt_format): - fps = self._format_frame_rate(defualt_format) - source_range = otio.opentime.TimeRange( - start_time=otio.opentime.RationalTime(start_frame, fps), - duration=otio.opentime.RationalTime(number_of_frames, fps) - ) - return otio.schema.Gap(source_range=source_range) - - def _timing_clip(self, clip): - while clip.tag not in ("clip", "asset-clip", "ref-clip"): - clip = self.child_parent_map.get(clip) - return clip - - def _offset_and_lane(self, clip, default_format): - clip_format_id = self._format_id_for_clip(clip, default_format) - clip = self._timing_clip(clip) - parent = self.child_parent_map.get(clip) - - parent_format_id = self._format_id_for_clip(parent, default_format) - - if parent.tag == "spine" and parent.get("lane", None): - lane = parent.get("lane") - parent = self.child_parent_map.get(parent) - spine = True - else: - lane = clip.get("lane", "0") - spine = False - - clip_offset_frames = self._number_of_frames( - clip.get("offset"), - clip_format_id - ) - - if spine: - parent_start_frames = 0 - else: - parent_start_frames = self._number_of_frames( - parent.get("start", None), - parent_format_id - ) - - parent_offset_frames = self._number_of_frames( - parent.get("offset", None), - parent_format_id - ) - - clip_offset_frames = ( - (int(clip_offset_frames) - int(parent_start_frames)) + - int(parent_offset_frames) - ) - - offset = otio.opentime.RationalTime( - clip_offset_frames, - self._format_frame_rate(clip_format_id) - ) - - return offset, lane - - def _format_id_for_clip(self, clip, default_format): - if not clip.get("ref", None) or clip.tag == "gap": - return default_format - - resource = self._asset_by_id(clip.get("ref")) - - if resource is None: - resource = self._compound_clip_by_id( - clip.get("ref") - ).find("sequence") - - return resource.get("format", default_format) - - def _reference_from_id(self, asset_id, default_format): - asset = self._asset_by_id(asset_id) - if not asset.get("src", ""): - return otio.schema.MissingReference() - - available_range = otio.opentime.TimeRange( - start_time=to_rational_time( - asset.get("start"), - self._format_frame_rate( - asset.get("format", default_format) - ) - ), - duration=to_rational_time( - asset.get("duration"), - self._format_frame_rate( - asset.get("format", default_format) - ) - ) - ) - asset_clip = self._assetclip_by_ref(asset_id) - metadata = {} - if asset_clip is not None: - metadata = self._create_metadta(asset_clip) - return otio.schema.ExternalReference( - target_url=asset.get("src"), - available_range=available_range, - metadata={"fcpx": metadata} - ) - - def _create_metadta(self, item): - metadata = {} - for element in item.iter(): - if element.tag == "md": - metadata.setdefault("metadata", []).append( - {element.attrib.get("key"): element.attrib.get("value")} - ) - # metadata.update( - # {element.attrib.get("key"): element.attrib.get("value")} - # ) - if element.tag == "note": - metadata.update({"note": element.text}) - if element.tag == "keyword": - metadata.setdefault("keywords", []).append(element.attrib) - return metadata - - # -------------------- - # time helpers - # -------------------- - def _format_frame_duration(self, format_id): - media_format = self._format_by_id(format_id) - total, rate = media_format.get("frameDuration").split("/") - rate = rate.replace("s", "") - return total, rate - - def _format_frame_rate(self, format_id): - fd_total, fd_rate = self._format_frame_duration(format_id) - return int(float(fd_rate) / float(fd_total)) - - def _number_of_frames(self, time_value, format_id): - if time_value == "0s" or time_value is None: - return 0 - fd_total, fd_rate = self._format_frame_duration(format_id) - time_value = time_value.split("/") - - if len(time_value) > 1: - time_value_a, time_value_b = time_value - return int( - (float(time_value_a) / float(time_value_b.replace("s", ""))) * - (float(fd_rate) / float(fd_total)) - ) - - return int( - int(time_value[0].replace("s", "")) * - (float(fd_rate) / float(fd_total)) - ) - - def _time_range(self, element, format_id): - return otio.opentime.TimeRange( - start_time=to_rational_time( - element.get("start", "0s"), - self._format_frame_rate(format_id) - ), - duration=to_rational_time( - element.get("duration"), - self._format_frame_rate(format_id) - ) - ) - # -------------------- - # search helpers - # -------------------- - - def _asset_by_id(self, asset_id): - return self.fcpx_xml.find( - f"./resources/asset[@id='{asset_id}']" - ) - - def _assetclip_by_ref(self, asset_id): - event = self.fcpx_xml.find("./event") - if event is None: - return self.fcpx_xml.find(f"./asset-clip[@ref='{asset_id}']") - else: - return event.find(f"./asset-clip[@ref='{asset_id}']") - - def _format_by_id(self, format_id): - return self.fcpx_xml.find( - f"./resources/format[@id='{format_id}']" - ) - - def _compound_clip_by_id(self, compound_id): - return self.fcpx_xml.find( - f"./resources/media[@id='{compound_id}']" - ) - - # -------------------- - # static methods - # -------------------- - @staticmethod - def _track_type(lane_items): - audio_only_items = [item for item in lane_items if item["audio_only"]] - if len(audio_only_items) == len(lane_items): - return otio.schema.TrackKind.Audio - return otio.schema.TrackKind.Video - - @staticmethod - def _sorted_items(lane, otio_objects): - lane_items = [item for item in otio_objects if item["track"] == lane] - return sorted(lane_items, key=lambda k: k["offset"]) - - -# -------------------- -# adapter requirements -# -------------------- -def read_from_string(input_str): - """ - Necessary read method for otio adapter - - Args: - input_str (str): An FCP X XML string - - Returns: - OpenTimeline: An OpenTimeline object - """ - - return FcpxXml(input_str).to_otio() - - -def write_to_string(input_otio): - """ - Necessary write method for otio adapter - - Args: - input_otio (OpenTimeline): An OpenTimeline object - - Returns: - str: The string contents of an FCP X XML - """ - - return FcpxOtio(input_otio).to_xml() diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_clips.fcpxml b/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_clips.fcpxml deleted file mode 100644 index a893cedf53..0000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_clips.fcpxml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - AAC - H.264 - - - - - - - - Truck in snow - - - - - - - - \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_event.fcpxml b/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_event.fcpxml deleted file mode 100644 index 746933860e..0000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_event.fcpxml +++ /dev/null @@ -1,379 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - AAC - H.264 - - - - - - - - - - - - - - - A simple note - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_example.fcpxml b/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_example.fcpxml deleted file mode 100644 index 8efbd67524..0000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_example.fcpxml +++ /dev/null @@ -1,309 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A simple note - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_library.fcpxml b/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_library.fcpxml deleted file mode 100644 index 8efbd67524..0000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_library.fcpxml +++ /dev/null @@ -1,309 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A simple note - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_project.fcpxml b/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_project.fcpxml deleted file mode 100644 index 4a43897837..0000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/sample_data/fcpx_project.fcpxml +++ /dev/null @@ -1 +0,0 @@ - AAC H.264 AAC H.264 AAC H.264 AAC H.264 AAC H.264 AAC H.264 AAC H.264 AAC H.264 \ No newline at end of file diff --git a/contrib/opentimelineio_contrib/adapters/tests/test_fcpx_adapter.py b/contrib/opentimelineio_contrib/adapters/tests/test_fcpx_adapter.py deleted file mode 100644 index 8eafbb58e0..0000000000 --- a/contrib/opentimelineio_contrib/adapters/tests/test_fcpx_adapter.py +++ /dev/null @@ -1,175 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright Contributors to the OpenTimelineIO project - -import os -import subprocess -import sys -import unittest -import unittest.mock -import opentimelineio as otio -import opentimelineio.test_utils as otio_test_utils -from opentimelineio_contrib.adapters.fcpx_xml import format_name - -SAMPLE_LIBRARY_XML = os.path.join( - os.path.dirname(__file__), - "sample_data", - "fcpx_library.fcpxml" -) -SAMPLE_PROJECT_XML = os.path.join( - os.path.dirname(__file__), - "sample_data", - "fcpx_project.fcpxml" -) -SAMPLE_EVENT_XML = os.path.join( - os.path.dirname(__file__), - "sample_data", - "fcpx_event.fcpxml" -) -SAMPLE_CLIPS_XML = os.path.join( - os.path.dirname(__file__), - "sample_data", - "fcpx_clips.fcpxml" -) - - -class AdaptersFcpXXmlTest(unittest.TestCase, otio_test_utils.OTIOAssertions): - """ - The test class for the FCP X XML adapter - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.maxDiff = None - - def test_library_roundtrip(self): - container = otio.adapters.read_from_file(SAMPLE_LIBRARY_XML) - timeline = container.find_children( - descended_from_type=otio.schema.Timeline)[0] - - self.assertIsNotNone(timeline) - self.assertEqual(len(timeline.tracks), 4) - - self.assertEqual(len(timeline.video_tracks()), 3) - self.assertEqual(len(timeline.audio_tracks()), 1) - - video_clip_names = ( - ( - 'IMG_0715', - "", - 'compound_clip_1', - 'IMG_0233', - 'IMG_0687', - 'IMG_0268', - 'compound_clip_1' - ), - ("", 'IMG_0513', "", 'IMG_0268', 'IMG_0740'), - ("", 'IMG_0857') - ) - - for n, track in enumerate(timeline.video_tracks()): - self.assertTupleEqual( - tuple(c.name for c in track), - video_clip_names[n] - ) - - fcpx_xml = otio.adapters.write_to_string(container, "fcpx_xml") - self.assertIsNotNone(fcpx_xml) - - new_timeline = otio.adapters.read_from_string(fcpx_xml, "fcpx_xml") - self.assertJsonEqual(container, new_timeline) - - def test_event_roundtrip(self): - container = otio.adapters.read_from_file(SAMPLE_EVENT_XML) - timeline = container.find_children( - descended_from_type=otio.schema.Timeline)[0] - - self.assertIsNotNone(timeline) - self.assertEqual(len(timeline.tracks), 4) - - self.assertEqual(len(timeline.video_tracks()), 3) - self.assertEqual(len(timeline.audio_tracks()), 1) - - video_clip_names = ( - ( - 'IMG_0715', - "", - 'compound_clip_1', - 'IMG_0233', - 'IMG_0687', - 'IMG_0268', - 'compound_clip_1' - ), - ("", 'IMG_0513', "", 'IMG_0268', 'IMG_0740'), - ("", 'IMG_0857') - ) - - for n, track in enumerate(timeline.video_tracks()): - self.assertTupleEqual( - tuple(c.name for c in track), - video_clip_names[n] - ) - - fcpx_xml = otio.adapters.write_to_string(container, "fcpx_xml") - self.assertIsNotNone(fcpx_xml) - - new_timeline = otio.adapters.read_from_string(fcpx_xml, "fcpx_xml") - self.assertJsonEqual(container, new_timeline) - - def test_project_roundtrip(self): - timeline = otio.adapters.read_from_file(SAMPLE_PROJECT_XML) - - self.assertIsNotNone(timeline) - self.assertEqual(len(timeline.tracks), 4) - - self.assertEqual(len(timeline.video_tracks()), 3) - self.assertEqual(len(timeline.audio_tracks()), 1) - - video_clip_names = ( - ( - 'IMG_0715', - "", - 'compound_clip_1', - 'IMG_0233', - 'IMG_0687', - 'IMG_0268', - 'compound_clip_1' - ), - ("", 'IMG_0513', "", 'IMG_0268', 'IMG_0740'), - ("", 'IMG_0857') - ) - - for n, track in enumerate(timeline.video_tracks()): - self.assertTupleEqual( - tuple(c.name for c in track), - video_clip_names[n] - ) - - fcpx_xml = otio.adapters.write_to_string(timeline, "fcpx_xml") - self.assertIsNotNone(fcpx_xml) - - new_timeline = otio.adapters.read_from_string(fcpx_xml, "fcpx_xml") - self.assertJsonEqual(timeline, new_timeline) - - def test_clips_roundtrip(self): - container = otio.adapters.read_from_file(SAMPLE_CLIPS_XML) - fcpx_xml = otio.adapters.write_to_string(container, "fcpx_xml") - self.assertIsNotNone(fcpx_xml) - - new_timeline = otio.adapters.read_from_string(fcpx_xml, "fcpx_xml") - self.assertJsonEqual(container, new_timeline) - - def test_format_name(self): - rvalue = subprocess.check_output( - [sys.executable, '-c', 'print("640x360")'] - ) - mock_patch = unittest.mock.patch.object - with mock_patch(subprocess, 'check_output', return_value=rvalue): - with mock_patch(os.path, 'exists', return_value=True): - self.assertEqual( - format_name(25, "file:///dummy.me"), - 'FFVideoFormat640x360p25' - ) - - -if __name__ == '__main__': - unittest.main() diff --git a/docs/tutorials/adapters.md b/docs/tutorials/adapters.md index c6d2d825d9..ddb4c4522e 100644 --- a/docs/tutorials/adapters.md +++ b/docs/tutorials/adapters.md @@ -9,10 +9,6 @@ Final Cut 7 XML Format - Status: Supported via the `fcp_xml` adapter - [Reference](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/FinalCutPro_XML/AboutThisDoc/AboutThisDoc.html#//apple_ref/doc/uid/TP30001152-TPXREF101) -Final Cut Pro X XML Format: -- Status: Supported via the `fcpx_xml` adapter -- [Intro to FCP X XML](https://developer.apple.com/library/mac/documentation/FinalCutProX/Reference/FinalCutProXXMLFormat/Introduction/Introduction.html) - ## Adobe Premiere Project - Based on guidance from Adobe, we support interchange with Adobe Premiere via diff --git a/docs/tutorials/otio-plugins.md b/docs/tutorials/otio-plugins.md index 976aa33ebd..6e6f6a02dd 100644 --- a/docs/tutorials/otio-plugins.md +++ b/docs/tutorials/otio-plugins.md @@ -286,44 +286,6 @@ required OTIO function hook -### fcpx_xml - -``` -OpenTimelineIO Final Cut Pro X XML Adapter. -``` - -*source*: `opentimelineio_contrib/adapters/fcpx_xml.py` - - -*Supported Features (with arguments)*: - -- read_from_string: -``` -Necessary read method for otio adapter - - Args: - input_str (str): An FCP X XML string - - Returns: - OpenTimeline: An OpenTimeline object -``` - - input_str -- write_to_string: -``` -Necessary write method for otio adapter - - Args: - input_otio (OpenTimeline): An OpenTimeline object - - Returns: - str: The string contents of an FCP X XML -``` - - input_otio - - - - - ### hls_playlist ```