diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py new file mode 100644 index 0000000000..77dc9c45b3 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportTask.py @@ -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\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) diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportUI.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportUI.py new file mode 100644 index 0000000000..887ff05ec8 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/OTIOExportUI.py @@ -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 +) diff --git a/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/__init__.py b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/__init__.py new file mode 100644 index 0000000000..67e6e78d35 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/Startup/otioexporter/__init__.py @@ -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' +] diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py new file mode 100644 index 0000000000..f506333a67 --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/OTIOImport.py @@ -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) diff --git a/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/__init__.py b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/__init__.py new file mode 100644 index 0000000000..1503a9e9ac --- /dev/null +++ b/setup/nukestudio/hiero_plugin_path/Python/StartupUI/otioimporter/__init__.py @@ -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 + )