mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
feat(nukestudio): adding otio plugin
This commit is contained in:
parent
7237741033
commit
38c693a792
5 changed files with 955 additions and 0 deletions
|
|
@ -0,0 +1,369 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import hiero.core
|
||||
from hiero.core import util
|
||||
|
||||
import opentimelineio as otio
|
||||
|
||||
|
||||
marker_color_map = {
|
||||
"magenta": otio.schema.MarkerColor.MAGENTA,
|
||||
"red": otio.schema.MarkerColor.RED,
|
||||
"yellow": otio.schema.MarkerColor.YELLOW,
|
||||
"green": otio.schema.MarkerColor.GREEN,
|
||||
"cyan": otio.schema.MarkerColor.CYAN,
|
||||
"blue": otio.schema.MarkerColor.BLUE,
|
||||
}
|
||||
|
||||
|
||||
class OTIOExportTask(hiero.core.TaskBase):
|
||||
|
||||
def __init__(self, initDict):
|
||||
"""Initialize"""
|
||||
hiero.core.TaskBase.__init__(self, initDict)
|
||||
|
||||
def name(self):
|
||||
return str(type(self))
|
||||
|
||||
def get_rate(self, item):
|
||||
num, den = item.framerate().toRational()
|
||||
rate = float(num) / float(den)
|
||||
|
||||
if rate.is_integer():
|
||||
return rate
|
||||
|
||||
return round(rate, 2)
|
||||
|
||||
def get_clip_ranges(self, trackitem):
|
||||
# Is clip an audio file? Use sequence frame rate
|
||||
if not trackitem.source().mediaSource().hasVideo():
|
||||
rate_item = trackitem.sequence()
|
||||
|
||||
else:
|
||||
rate_item = trackitem.source()
|
||||
|
||||
source_rate = self.get_rate(rate_item)
|
||||
|
||||
# Reversed video/audio
|
||||
if trackitem.playbackSpeed() < 0:
|
||||
start = trackitem.sourceOut()
|
||||
|
||||
else:
|
||||
start = trackitem.sourceIn()
|
||||
|
||||
source_start_time = otio.opentime.RationalTime(
|
||||
start,
|
||||
source_rate
|
||||
)
|
||||
source_duration = otio.opentime.RationalTime(
|
||||
trackitem.duration(),
|
||||
source_rate
|
||||
)
|
||||
|
||||
source_range = otio.opentime.TimeRange(
|
||||
start_time=source_start_time,
|
||||
duration=source_duration
|
||||
)
|
||||
|
||||
available_range = None
|
||||
hiero_clip = trackitem.source()
|
||||
if not hiero_clip.mediaSource().isOffline():
|
||||
start_time = otio.opentime.RationalTime(
|
||||
hiero_clip.mediaSource().startTime(),
|
||||
source_rate
|
||||
)
|
||||
duration = otio.opentime.RationalTime(
|
||||
hiero_clip.mediaSource().duration(),
|
||||
source_rate
|
||||
)
|
||||
available_range = otio.opentime.TimeRange(
|
||||
start_time=start_time,
|
||||
duration=duration
|
||||
)
|
||||
|
||||
return source_range, available_range
|
||||
|
||||
def add_gap(self, trackitem, otio_track, prev_out):
|
||||
gap_length = trackitem.timelineIn() - prev_out
|
||||
if prev_out != 0:
|
||||
gap_length -= 1
|
||||
|
||||
rate = self.get_rate(trackitem.sequence())
|
||||
gap = otio.opentime.TimeRange(
|
||||
duration=otio.opentime.RationalTime(
|
||||
gap_length,
|
||||
rate
|
||||
)
|
||||
)
|
||||
otio_gap = otio.schema.Gap(source_range=gap)
|
||||
otio_track.append(otio_gap)
|
||||
|
||||
def get_marker_color(self, tag):
|
||||
icon = tag.icon()
|
||||
pat = 'icons:Tag(?P<color>\w+)\.\w+'
|
||||
|
||||
res = re.search(pat, icon)
|
||||
if res:
|
||||
color = res.groupdict().get('color')
|
||||
if color.lower() in marker_color_map:
|
||||
return marker_color_map[color.lower()]
|
||||
|
||||
return otio.schema.MarkerColor.RED
|
||||
|
||||
def add_markers(self, hiero_item, otio_item):
|
||||
for tag in hiero_item.tags():
|
||||
if not tag.visible():
|
||||
continue
|
||||
|
||||
if tag.name() == 'Copy':
|
||||
# Hiero adds this tag to a lot of clips
|
||||
continue
|
||||
|
||||
frame_rate = self.get_rate(hiero_item)
|
||||
|
||||
marked_range = otio.opentime.TimeRange(
|
||||
start_time=otio.opentime.RationalTime(
|
||||
tag.inTime(),
|
||||
frame_rate
|
||||
),
|
||||
duration=otio.opentime.RationalTime(
|
||||
int(tag.metadata().dict().get('tag.length', '0')),
|
||||
frame_rate
|
||||
)
|
||||
)
|
||||
|
||||
marker = otio.schema.Marker(
|
||||
name=tag.name(),
|
||||
color=self.get_marker_color(tag),
|
||||
marked_range=marked_range,
|
||||
metadata={
|
||||
'Hiero': tag.metadata().dict()
|
||||
}
|
||||
)
|
||||
|
||||
otio_item.markers.append(marker)
|
||||
|
||||
def add_clip(self, trackitem, otio_track, itemindex):
|
||||
hiero_clip = trackitem.source()
|
||||
|
||||
# Add Gap if needed
|
||||
prev_item = (
|
||||
itemindex and trackitem.parent().items()[itemindex - 1] or
|
||||
trackitem
|
||||
)
|
||||
|
||||
if prev_item == trackitem and trackitem.timelineIn() > 0:
|
||||
self.add_gap(trackitem, otio_track, 0)
|
||||
|
||||
elif (
|
||||
prev_item != trackitem and
|
||||
prev_item.timelineOut() != trackitem.timelineIn()
|
||||
):
|
||||
self.add_gap(trackitem, otio_track, prev_item.timelineOut())
|
||||
|
||||
# Create Clip
|
||||
source_range, available_range = self.get_clip_ranges(trackitem)
|
||||
|
||||
otio_clip = otio.schema.Clip()
|
||||
otio_clip.name = trackitem.name()
|
||||
otio_clip.source_range = source_range
|
||||
|
||||
# Add media reference
|
||||
media_reference = otio.schema.MissingReference()
|
||||
if not hiero_clip.mediaSource().isOffline():
|
||||
source = hiero_clip.mediaSource()
|
||||
media_reference = otio.schema.ExternalReference()
|
||||
media_reference.available_range = available_range
|
||||
|
||||
path, name = os.path.split(source.fileinfos()[0].filename())
|
||||
media_reference.target_url = os.path.join(path, name)
|
||||
media_reference.name = name
|
||||
|
||||
otio_clip.media_reference = media_reference
|
||||
|
||||
# Add Time Effects
|
||||
playbackspeed = trackitem.playbackSpeed()
|
||||
if playbackspeed != 1:
|
||||
if playbackspeed == 0:
|
||||
time_effect = otio.schema.FreezeFrame()
|
||||
|
||||
else:
|
||||
time_effect = otio.schema.LinearTimeWarp(
|
||||
time_scalar=playbackspeed
|
||||
)
|
||||
otio_clip.effects.append(time_effect)
|
||||
|
||||
# Add tags as markers
|
||||
if self._preset.properties()["includeTags"]:
|
||||
self.add_markers(trackitem.source(), otio_clip)
|
||||
|
||||
otio_track.append(otio_clip)
|
||||
|
||||
# Add Transition if needed
|
||||
if trackitem.inTransition() or trackitem.outTransition():
|
||||
self.add_transition(trackitem, otio_track)
|
||||
|
||||
def add_transition(self, trackitem, otio_track):
|
||||
transitions = []
|
||||
|
||||
if trackitem.inTransition():
|
||||
if trackitem.inTransition().alignment().name == 'kFadeIn':
|
||||
transitions.append(trackitem.inTransition())
|
||||
|
||||
if trackitem.outTransition():
|
||||
transitions.append(trackitem.outTransition())
|
||||
|
||||
for transition in transitions:
|
||||
alignment = transition.alignment().name
|
||||
|
||||
if alignment == 'kFadeIn':
|
||||
in_offset_frames = 0
|
||||
out_offset_frames = (
|
||||
transition.timelineOut() - transition.timelineIn()
|
||||
) + 1
|
||||
|
||||
elif alignment == 'kFadeOut':
|
||||
in_offset_frames = (
|
||||
trackitem.timelineOut() - transition.timelineIn()
|
||||
) + 1
|
||||
out_offset_frames = 0
|
||||
|
||||
elif alignment == 'kDissolve':
|
||||
in_offset_frames = (
|
||||
transition.inTrackItem().timelineOut() -
|
||||
transition.timelineIn()
|
||||
)
|
||||
out_offset_frames = (
|
||||
transition.timelineOut() -
|
||||
transition.outTrackItem().timelineIn()
|
||||
)
|
||||
|
||||
else:
|
||||
# kUnknown transition is ignored
|
||||
continue
|
||||
|
||||
rate = trackitem.source().framerate().toFloat()
|
||||
in_time = otio.opentime.RationalTime(in_offset_frames, rate)
|
||||
out_time = otio.opentime.RationalTime(out_offset_frames, rate)
|
||||
|
||||
otio_transition = otio.schema.Transition(
|
||||
name=alignment, # Consider placing Hiero name in metadata
|
||||
transition_type=otio.schema.TransitionTypes.SMPTE_Dissolve,
|
||||
in_offset=in_time,
|
||||
out_offset=out_time,
|
||||
metadata={}
|
||||
)
|
||||
|
||||
if alignment == 'kFadeIn':
|
||||
otio_track.insert(-2, otio_transition)
|
||||
|
||||
else:
|
||||
otio_track.append(otio_transition)
|
||||
|
||||
def add_tracks(self):
|
||||
for track in self._sequence.items():
|
||||
if isinstance(track, hiero.core.AudioTrack):
|
||||
kind = otio.schema.TrackKind.Audio
|
||||
|
||||
else:
|
||||
kind = otio.schema.TrackKind.Video
|
||||
|
||||
otio_track = otio.schema.Track(kind=kind)
|
||||
otio_track.name = track.name()
|
||||
|
||||
for itemindex, trackitem in enumerate(track):
|
||||
if isinstance(trackitem.source(), hiero.core.Clip):
|
||||
self.add_clip(trackitem, otio_track, itemindex)
|
||||
|
||||
self.otio_timeline.tracks.append(otio_track)
|
||||
|
||||
# Add tags as markers
|
||||
if self._preset.properties()["includeTags"]:
|
||||
self.add_markers(self._sequence, self.otio_timeline.tracks)
|
||||
|
||||
def create_OTIO(self):
|
||||
self.otio_timeline = otio.schema.Timeline()
|
||||
self.otio_timeline.name = self._sequence.name()
|
||||
|
||||
self.add_tracks()
|
||||
|
||||
def startTask(self):
|
||||
self.create_OTIO()
|
||||
|
||||
def taskStep(self):
|
||||
return False
|
||||
|
||||
def finishTask(self):
|
||||
try:
|
||||
exportPath = self.resolvedExportPath()
|
||||
|
||||
# Check file extension
|
||||
if not exportPath.lower().endswith(".otio"):
|
||||
exportPath += ".otio"
|
||||
|
||||
# check export root exists
|
||||
dirname = os.path.dirname(exportPath)
|
||||
util.filesystem.makeDirs(dirname)
|
||||
|
||||
# write otio file
|
||||
otio.adapters.write_to_file(self.otio_timeline, exportPath)
|
||||
|
||||
# Catch all exceptions and log error
|
||||
except Exception as e:
|
||||
self.setError("failed to write file {f}\n{e}".format(
|
||||
f=exportPath,
|
||||
e=e)
|
||||
)
|
||||
|
||||
hiero.core.TaskBase.finishTask(self)
|
||||
|
||||
def forcedAbort(self):
|
||||
pass
|
||||
|
||||
|
||||
class OTIOExportPreset(hiero.core.TaskPresetBase):
|
||||
def __init__(self, name, properties):
|
||||
"""Initialise presets to default values"""
|
||||
hiero.core.TaskPresetBase.__init__(self, OTIOExportTask, name)
|
||||
|
||||
self.properties()["includeTags"] = True
|
||||
self.properties().update(properties)
|
||||
|
||||
def supportedItems(self):
|
||||
return hiero.core.TaskPresetBase.kSequence
|
||||
|
||||
def addCustomResolveEntries(self, resolver):
|
||||
resolver.addResolver(
|
||||
"{ext}",
|
||||
"Extension of the file to be output",
|
||||
lambda keyword, task: "otio"
|
||||
)
|
||||
|
||||
def supportsAudio(self):
|
||||
return True
|
||||
|
||||
|
||||
hiero.core.taskRegistry.registerTask(OTIOExportPreset, OTIOExportTask)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import hiero.ui
|
||||
import OTIOExportTask
|
||||
|
||||
try:
|
||||
# Hiero >= 11.x
|
||||
from PySide2 import QtCore
|
||||
from PySide2.QtWidgets import QCheckBox
|
||||
from hiero.ui.FnTaskUIFormLayout import TaskUIFormLayout as FormLayout
|
||||
|
||||
except ImportError:
|
||||
# Hiero <= 10.x
|
||||
from PySide import QtCore # lint:ok
|
||||
from PySide.QtGui import QCheckBox, QFormLayout # lint:ok
|
||||
|
||||
FormLayout = QFormLayout # lint:ok
|
||||
|
||||
|
||||
class OTIOExportUI(hiero.ui.TaskUIBase):
|
||||
def __init__(self, preset):
|
||||
"""Initialize"""
|
||||
hiero.ui.TaskUIBase.__init__(
|
||||
self,
|
||||
OTIOExportTask.OTIOExportTask,
|
||||
preset,
|
||||
"OTIO Exporter"
|
||||
)
|
||||
|
||||
def includeMarkersCheckboxChanged(self, state):
|
||||
# Slot to handle change of checkbox state
|
||||
self._preset.properties()["includeTags"] = state == QtCore.Qt.Checked
|
||||
|
||||
def populateUI(self, widget, exportTemplate):
|
||||
layout = widget.layout()
|
||||
formLayout = FormLayout()
|
||||
|
||||
# Hiero ~= 10.0v4
|
||||
if layout is None:
|
||||
layout = formLayout
|
||||
widget.setLayout(layout)
|
||||
|
||||
else:
|
||||
layout.addLayout(formLayout)
|
||||
|
||||
# Checkboxes for whether the OTIO should contain markers or not
|
||||
self.includeMarkersCheckbox = QCheckBox()
|
||||
self.includeMarkersCheckbox.setToolTip(
|
||||
"Enable to include Tags as markers in the exported OTIO file."
|
||||
)
|
||||
self.includeMarkersCheckbox.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
if self._preset.properties()["includeTags"]:
|
||||
self.includeMarkersCheckbox.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
self.includeMarkersCheckbox.stateChanged.connect(
|
||||
self.includeMarkersCheckboxChanged
|
||||
)
|
||||
|
||||
# Add Checkbox to layout
|
||||
formLayout.addRow("Include Tags:", self.includeMarkersCheckbox)
|
||||
|
||||
|
||||
hiero.ui.taskUIRegistry.registerTaskUI(
|
||||
OTIOExportTask.OTIOExportPreset,
|
||||
OTIOExportUI
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from OTIOExportTask import OTIOExportTask
|
||||
from OTIOExportUI import OTIOExportUI
|
||||
|
||||
__all__ = [
|
||||
'OTIOExportTask',
|
||||
'OTIOExportUI'
|
||||
]
|
||||
|
|
@ -0,0 +1,435 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import hiero.core
|
||||
import hiero.ui
|
||||
|
||||
try:
|
||||
from urllib import unquote
|
||||
|
||||
except ImportError:
|
||||
from urllib.parse import unquote # lint:ok
|
||||
|
||||
import opentimelineio as otio
|
||||
|
||||
|
||||
def get_transition_type(otio_item, otio_track):
|
||||
_in, _out = otio_track.neighbors_of(otio_item)
|
||||
|
||||
if isinstance(_in, otio.schema.Gap):
|
||||
_in = None
|
||||
|
||||
if isinstance(_out, otio.schema.Gap):
|
||||
_out = None
|
||||
|
||||
if _in and _out:
|
||||
return 'dissolve'
|
||||
|
||||
elif _in and not _out:
|
||||
return 'fade_out'
|
||||
|
||||
elif not _in and _out:
|
||||
return 'fade_in'
|
||||
|
||||
else:
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def find_trackitem(name, hiero_track):
|
||||
for item in hiero_track.items():
|
||||
if item.name() == name:
|
||||
return item
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_neighboring_trackitems(otio_item, otio_track, hiero_track):
|
||||
_in, _out = otio_track.neighbors_of(otio_item)
|
||||
trackitem_in = None
|
||||
trackitem_out = None
|
||||
|
||||
if _in:
|
||||
trackitem_in = find_trackitem(_in.name, hiero_track)
|
||||
|
||||
if _out:
|
||||
trackitem_out = find_trackitem(_out.name, hiero_track)
|
||||
|
||||
return trackitem_in, trackitem_out
|
||||
|
||||
|
||||
def apply_transition(otio_track, otio_item, track):
|
||||
# Figure out type of transition
|
||||
transition_type = get_transition_type(otio_item, otio_track)
|
||||
|
||||
# Figure out track kind for getattr below
|
||||
if isinstance(track, hiero.core.VideoTrack):
|
||||
kind = ''
|
||||
|
||||
else:
|
||||
kind = 'Audio'
|
||||
|
||||
try:
|
||||
# Gather TrackItems involved in trasition
|
||||
item_in, item_out = get_neighboring_trackitems(
|
||||
otio_item,
|
||||
otio_track,
|
||||
track
|
||||
)
|
||||
|
||||
# Create transition object
|
||||
if transition_type == 'dissolve':
|
||||
transition_func = getattr(
|
||||
hiero.core.Transition,
|
||||
'create{kind}DissolveTransition'.format(kind=kind)
|
||||
)
|
||||
|
||||
transition = transition_func(
|
||||
item_in,
|
||||
item_out,
|
||||
otio_item.in_offset.value,
|
||||
otio_item.out_offset.value
|
||||
)
|
||||
|
||||
elif transition_type == 'fade_in':
|
||||
transition_func = getattr(
|
||||
hiero.core.Transition,
|
||||
'create{kind}FadeInTransition'.format(kind=kind)
|
||||
)
|
||||
transition = transition_func(
|
||||
item_out,
|
||||
otio_item.out_offset.value
|
||||
)
|
||||
|
||||
elif transition_type == 'fade_out':
|
||||
transition_func = getattr(
|
||||
hiero.core.Transition,
|
||||
'create{kind}FadeOutTransition'.format(kind=kind)
|
||||
)
|
||||
transition = transition_func(
|
||||
item_in,
|
||||
otio_item.in_offset.value
|
||||
)
|
||||
|
||||
else:
|
||||
# Unknown transition
|
||||
return
|
||||
|
||||
# Apply transition to track
|
||||
track.addTransition(transition)
|
||||
|
||||
except Exception, e:
|
||||
sys.stderr.write(
|
||||
'Unable to apply transition "{t}": "{e}"\n'.format(
|
||||
t=otio_item,
|
||||
e=e
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def prep_url(url_in):
|
||||
url = unquote(url_in)
|
||||
|
||||
if url.startswith('file://localhost/'):
|
||||
return url.replace('file://localhost/', '')
|
||||
|
||||
url = '{url}'.format(
|
||||
sep=url.startswith(os.sep) and '' or os.sep,
|
||||
url=url.startswith(os.sep) and url[1:] or url
|
||||
)
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def create_offline_mediasource(otio_clip, path=None):
|
||||
hiero_rate = hiero.core.TimeBase(
|
||||
otio_clip.source_range.start_time.rate
|
||||
)
|
||||
|
||||
if isinstance(otio_clip.media_reference, otio.schema.ExternalReference):
|
||||
source_range = otio_clip.available_range()
|
||||
|
||||
else:
|
||||
source_range = otio_clip.source_range
|
||||
|
||||
if path is None:
|
||||
path = otio_clip.name
|
||||
|
||||
media = hiero.core.MediaSource.createOfflineVideoMediaSource(
|
||||
prep_url(path),
|
||||
source_range.start_time.value,
|
||||
source_range.duration.value,
|
||||
hiero_rate,
|
||||
source_range.start_time.value
|
||||
)
|
||||
|
||||
return media
|
||||
|
||||
|
||||
def load_otio(otio_file):
|
||||
otio_timeline = otio.adapters.read_from_file(otio_file)
|
||||
build_sequence(otio_timeline)
|
||||
|
||||
|
||||
marker_color_map = {
|
||||
"PINK": "Magenta",
|
||||
"RED": "Red",
|
||||
"ORANGE": "Yellow",
|
||||
"YELLOW": "Yellow",
|
||||
"GREEN": "Green",
|
||||
"CYAN": "Cyan",
|
||||
"BLUE": "Blue",
|
||||
"PURPLE": "Magenta",
|
||||
"MAGENTA": "Magenta",
|
||||
"BLACK": "Blue",
|
||||
"WHITE": "Green"
|
||||
}
|
||||
|
||||
|
||||
def get_tag(tagname, tagsbin):
|
||||
for tag in tagsbin.items():
|
||||
if tag.name() == tagname:
|
||||
return tag
|
||||
|
||||
if isinstance(tag, hiero.core.Bin):
|
||||
tag = get_tag(tagname, tag)
|
||||
|
||||
if tag is not None:
|
||||
return tag
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def add_metadata(metadata, hiero_item):
|
||||
for key, value in metadata.items():
|
||||
if isinstance(value, dict):
|
||||
add_metadata(value, hiero_item)
|
||||
continue
|
||||
|
||||
if value is not None:
|
||||
if not key.startswith('tag.'):
|
||||
key = 'tag.' + key
|
||||
|
||||
hiero_item.metadata().setValue(key, str(value))
|
||||
|
||||
|
||||
def add_markers(otio_item, hiero_item, tagsbin):
|
||||
if isinstance(otio_item, (otio.schema.Stack, otio.schema.Clip)):
|
||||
markers = otio_item.markers
|
||||
|
||||
elif isinstance(otio_item, otio.schema.Timeline):
|
||||
markers = otio_item.tracks.markers
|
||||
|
||||
else:
|
||||
markers = []
|
||||
|
||||
for marker in markers:
|
||||
marker_color = marker.color
|
||||
|
||||
_tag = get_tag(marker.name, tagsbin)
|
||||
if _tag is None:
|
||||
_tag = get_tag(marker_color_map[marker_color], tagsbin)
|
||||
|
||||
if _tag is None:
|
||||
_tag = hiero.core.Tag(marker_color_map[marker.color])
|
||||
|
||||
start = marker.marked_range.start_time.value
|
||||
end = (
|
||||
marker.marked_range.start_time.value +
|
||||
marker.marked_range.duration.value
|
||||
)
|
||||
|
||||
tag = hiero_item.addTagToRange(_tag, start, end)
|
||||
tag.setName(marker.name or marker_color_map[marker_color])
|
||||
|
||||
# Add metadata
|
||||
add_metadata(marker.metadata, tag)
|
||||
|
||||
|
||||
def create_track(otio_track, tracknum, track_kind):
|
||||
# Add track kind when dealing with nested stacks
|
||||
if isinstance(otio_track, otio.schema.Stack):
|
||||
otio_track.kind = track_kind
|
||||
|
||||
# Create a Track
|
||||
if otio_track.kind == otio.schema.TrackKind.Video:
|
||||
track = hiero.core.VideoTrack(
|
||||
otio_track.name or 'Video{n}'.format(n=tracknum)
|
||||
)
|
||||
|
||||
else:
|
||||
track = hiero.core.AudioTrack(
|
||||
otio_track.name or 'Audio{n}'.format(n=tracknum)
|
||||
)
|
||||
|
||||
return track
|
||||
|
||||
|
||||
def create_clip(otio_clip, tagsbin):
|
||||
# Create MediaSource
|
||||
otio_media = otio_clip.media_reference
|
||||
if isinstance(otio_media, otio.schema.ExternalReference):
|
||||
url = prep_url(otio_media.target_url)
|
||||
media = hiero.core.MediaSource(url)
|
||||
if media.isOffline():
|
||||
media = create_offline_mediasource(otio_clip, url)
|
||||
|
||||
else:
|
||||
media = create_offline_mediasource(otio_clip)
|
||||
|
||||
# Create Clip
|
||||
clip = hiero.core.Clip(media)
|
||||
|
||||
# Add markers
|
||||
add_markers(otio_clip, clip, tagsbin)
|
||||
|
||||
return clip
|
||||
|
||||
|
||||
def create_trackitem(playhead, track, otio_clip, clip):
|
||||
source_range = otio_clip.source_range
|
||||
|
||||
trackitem = track.createTrackItem(otio_clip.name)
|
||||
trackitem.setPlaybackSpeed(source_range.start_time.rate)
|
||||
trackitem.setSource(clip)
|
||||
|
||||
# Check for speed effects and adjust playback speed accordingly
|
||||
for effect in otio_clip.effects:
|
||||
if isinstance(effect, otio.schema.LinearTimeWarp):
|
||||
trackitem.setPlaybackSpeed(
|
||||
trackitem.playbackSpeed() *
|
||||
effect.time_scalar
|
||||
)
|
||||
|
||||
# If reverse playback speed swap source in and out
|
||||
if trackitem.playbackSpeed() < 0:
|
||||
source_out = source_range.start_time.value
|
||||
source_in = (
|
||||
source_range.start_time.value +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
timeline_in = playhead + source_out
|
||||
timeline_out = (
|
||||
timeline_in +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
else:
|
||||
# Normal playback speed
|
||||
source_in = source_range.start_time.value
|
||||
source_out = (
|
||||
source_range.start_time.value +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
timeline_in = playhead
|
||||
timeline_out = (
|
||||
timeline_in +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
|
||||
# Set source and timeline in/out points
|
||||
trackitem.setSourceIn(source_in)
|
||||
trackitem.setSourceOut(source_out)
|
||||
trackitem.setTimelineIn(timeline_in)
|
||||
trackitem.setTimelineOut(timeline_out)
|
||||
|
||||
return trackitem
|
||||
|
||||
|
||||
def build_sequence(otio_timeline, project=None, track_kind=None):
|
||||
if project is None:
|
||||
# TODO: Find a proper way for active project
|
||||
project = hiero.core.projects(hiero.core.Project.kUserProjects)[-1]
|
||||
|
||||
# Create a Sequence
|
||||
sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence')
|
||||
|
||||
# Create a Bin to hold clips
|
||||
projectbin = project.clipsBin()
|
||||
projectbin.addItem(hiero.core.BinItem(sequence))
|
||||
sequencebin = hiero.core.Bin(sequence.name())
|
||||
projectbin.addItem(sequencebin)
|
||||
|
||||
# Get tagsBin
|
||||
tagsbin = hiero.core.project("Tag Presets").tagsBin()
|
||||
|
||||
# Add timeline markers
|
||||
add_markers(otio_timeline, sequence, tagsbin)
|
||||
|
||||
# TODO: Set sequence settings from otio timeline if available
|
||||
if isinstance(otio_timeline, otio.schema.Timeline):
|
||||
tracks = otio_timeline.tracks
|
||||
|
||||
else:
|
||||
# otio.schema.Stack
|
||||
tracks = otio_timeline
|
||||
|
||||
for tracknum, otio_track in enumerate(tracks):
|
||||
playhead = 0
|
||||
_transitions = []
|
||||
|
||||
# Add track to sequence
|
||||
track = create_track(otio_track, tracknum, track_kind)
|
||||
sequence.addTrack(track)
|
||||
|
||||
# iterate over items in track
|
||||
for itemnum, otio_clip in enumerate(otio_track):
|
||||
if isinstance(otio_clip, otio.schema.Stack):
|
||||
bar = hiero.ui.mainWindow().statusBar()
|
||||
bar.showMessage(
|
||||
'Nested sequences are created separately.',
|
||||
timeout=3000
|
||||
)
|
||||
build_sequence(otio_clip, project, otio_track.kind)
|
||||
|
||||
elif isinstance(otio_clip, otio.schema.Clip):
|
||||
# Create a Clip
|
||||
clip = create_clip(otio_clip, tagsbin)
|
||||
|
||||
# Add Clip to a Bin
|
||||
sequencebin.addItem(hiero.core.BinItem(clip))
|
||||
|
||||
# Create TrackItem
|
||||
trackitem = create_trackitem(
|
||||
playhead,
|
||||
track,
|
||||
otio_clip,
|
||||
clip
|
||||
)
|
||||
|
||||
# Add trackitem to track
|
||||
track.addTrackItem(trackitem)
|
||||
|
||||
# Update playhead
|
||||
playhead = trackitem.timelineOut() + 1
|
||||
|
||||
elif isinstance(otio_clip, otio.schema.Transition):
|
||||
# Store transitions for when all clips in the track are created
|
||||
_transitions.append((otio_track, otio_clip))
|
||||
|
||||
elif isinstance(otio_clip, otio.schema.Gap):
|
||||
# Hiero has no fillers, slugs or blanks at the moment
|
||||
playhead += otio_clip.source_range.duration.value
|
||||
|
||||
# Apply transitions we stored earlier now that all clips are present
|
||||
for otio_track, otio_item in _transitions:
|
||||
apply_transition(otio_track, otio_item, track)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import hiero.ui
|
||||
import hiero.core
|
||||
|
||||
from otioimporter.OTIOImport import load_otio
|
||||
|
||||
|
||||
def OTIO_menu_action(event):
|
||||
otio_action = hiero.ui.createMenuAction(
|
||||
'Import OTIO',
|
||||
open_otio_file,
|
||||
icon=None
|
||||
)
|
||||
hiero.ui.registerAction(otio_action)
|
||||
for action in event.menu.actions():
|
||||
if action.text() == 'Import':
|
||||
action.menu().addAction(otio_action)
|
||||
break
|
||||
|
||||
|
||||
def open_otio_file():
|
||||
files = hiero.ui.openFileBrowser(
|
||||
caption='Please select an OTIO file of choice',
|
||||
pattern='*.otio',
|
||||
requiredExtension='.otio'
|
||||
)
|
||||
for otio_file in files:
|
||||
load_otio(otio_file)
|
||||
|
||||
|
||||
# HieroPlayer is quite limited and can't create transitions etc.
|
||||
if not hiero.core.isHieroPlayer():
|
||||
hiero.core.events.registerInterest(
|
||||
"kShowContextMenu/kBin",
|
||||
OTIO_menu_action
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue