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
```