Hiero: otio-workflow wip

This commit is contained in:
Jakub Jezek 2021-04-08 10:52:15 +02:00
parent 1ab3fb3953
commit 6c27152eeb
No known key found for this signature in database
GPG key ID: C4B96E101D2A47F3
8 changed files with 1045 additions and 367 deletions

View file

View file

@ -0,0 +1,386 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Daniel Flehner Heen"
__credits__ = ["Jakub Jezek", "Daniel Flehner Heen"]
import os
import sys
import re
import hiero.core
import hiero.ui
import opentimelineio as otio
# build modul class
self = sys.modules[__name__]
self.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,
}
self.hiero_sequence = None
self.include_tags = None
def get_rate(item):
if not hasattr(item, 'framerate'):
item = item.sequence()
num, den = item.framerate().toRational()
rate = float(num) / float(den)
if rate.is_integer():
return rate
return round(rate, 2)
def get_clip_ranges(trackitem):
# Get rate from source or sequence
if trackitem.source().mediaSource().hasVideo():
rate_item = trackitem.source()
else:
rate_item = trackitem.sequence()
source_rate = 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
)
hiero_clip = trackitem.source()
available_range = None
if hiero_clip.mediaSource().isMediaPresent():
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(trackitem, otio_track, prev_out):
gap_length = trackitem.timelineIn() - prev_out
if prev_out != 0:
gap_length -= 1
rate = 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(tag):
icon = tag.icon()
pat = r'icons:Tag(?P<color>\w+)\.\w+'
res = re.search(pat, icon)
if res:
color = res.groupdict().get('color')
if color.lower() in self.marker_color_map:
return self.marker_color_map[color.lower()]
return otio.schema.MarkerColor.RED
def add_markers(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 = 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
)
)
metadata = dict(
Hiero=tag.metadata().dict()
)
# Store the source item for future import assignment
metadata['Hiero']['source_type'] = hiero_item.__class__.__name__
marker = otio.schema.Marker(
name=tag.name(),
color=get_marker_color(tag),
marked_range=marked_range,
metadata=metadata
)
otio_item.markers.append(marker)
def add_clip(trackitem, otio_track, itemindex):
hiero_clip = trackitem.source()
# Add Gap if needed
if itemindex == 0:
prev_item = trackitem
else:
prev_item = trackitem.parent().items()[itemindex - 1]
clip_diff = trackitem.timelineIn() - prev_item.timelineOut()
if itemindex == 0 and trackitem.timelineIn() > 0:
add_gap(trackitem, otio_track, 0)
elif itemindex and clip_diff != 1:
add_gap(trackitem, otio_track, prev_item.timelineOut())
# Create Clip
source_range, available_range = get_clip_ranges(trackitem)
otio_clip = otio.schema.Clip(
name=trackitem.name(),
source_range=source_range
)
media_reference = create_otio_reference(hiero_clip)
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.include_tags:
add_markers(trackitem, otio_clip)
add_markers(trackitem.source(), otio_clip)
otio_track.append(otio_clip)
# Add Transition if needed
if trackitem.inTransition() or trackitem.outTransition():
add_transition(trackitem, otio_track)
def _get_metadata(hiero_object):
metadata = hiero_object.metadata()
return {key: value for key, value in metadata.items()}
def create_otio_reference(hiero_clip):
metadata = _get_metadata(hiero_clip)
mp_clip_property = media_pool_item.GetClipProperty()
path = mp_clip_property["File Path"]
reformat_path = utils.get_reformated_path(path, padded=True)
padding = utils.get_padding_from_path(path)
if padding:
metadata.update({
"isSequence": True,
"padding": padding
})
# get clip property regarding to type
mp_clip_property = media_pool_item.GetClipProperty()
fps = float(mp_clip_property["FPS"])
if mp_clip_property["Type"] == "Video":
frame_start = int(mp_clip_property["Start"])
frame_duration = int(mp_clip_property["Frames"])
else:
audio_duration = str(mp_clip_property["Duration"])
frame_start = 0
frame_duration = int(utils.timecode_to_frames(
audio_duration, float(fps)))
otio_ex_ref_item = None
if padding:
# if it is file sequence try to create `ImageSequenceReference`
# the OTIO might not be compatible so return nothing and do it old way
try:
dirname, filename = os.path.split(path)
collection = clique.parse(filename, '{head}[{ranges}]{tail}')
padding_num = len(re.findall("(\\d+)(?=-)", filename).pop())
otio_ex_ref_item = otio.schema.ImageSequenceReference(
target_url_base=dirname + os.sep,
name_prefix=collection.format("{head}"),
name_suffix=collection.format("{tail}"),
start_frame=frame_start,
frame_zero_padding=padding_num,
rate=fps,
available_range=create_otio_time_range(
frame_start,
frame_duration,
fps
)
)
except AttributeError:
pass
if not otio_ex_ref_item:
# in case old OTIO or video file create `ExternalReference`
otio_ex_ref_item = otio.schema.ExternalReference(
target_url=reformat_path,
available_range=create_otio_time_range(
frame_start,
frame_duration,
fps
)
)
# add metadata to otio item
add_otio_metadata(otio_ex_ref_item, hiero_clip, **metadata)
return otio_ex_ref_item
def add_otio_metadata(otio_item, hiero_clip, **kwargs):
mp_metadata = hiero_clip.GetMetadata()
# add additional metadata from kwargs
if kwargs:
mp_metadata.update(kwargs)
# add metadata to otio item metadata
for key, value in mp_metadata.items():
otio_item.metadata.update({key: value})
def add_transition(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
)
if alignment == 'kFadeIn':
otio_track.insert(-1, otio_transition)
else:
otio_track.append(otio_transition)
def add_tracks():
for track in self.hiero_sequence.items():
if isinstance(track, hiero.core.AudioTrack):
kind = otio.schema.TrackKind.Audio
else:
kind = otio.schema.TrackKind.Video
otio_track = otio.schema.Track(name=track.name(), kind=kind)
for itemindex, trackitem in enumerate(track):
if isinstance(trackitem.source(), hiero.core.Clip):
add_clip(trackitem, otio_track, itemindex)
self.otio_timeline.tracks.append(otio_track)
# Add tags as markers
if self.include_tags:
add_markers(self.hiero_sequence, self.otio_timeline.tracks)
def create_OTIO(sequence=None):
self.hiero_sequence = sequence or hiero.ui.activeSequence()
self.otio_timeline = otio.schema.Timeline()
# Set global start time based on sequence
self.otio_timeline.global_start_time = otio.opentime.RationalTime(
self.hiero_sequence.timecodeStart(),
self.hiero_sequence.framerate().toFloat()
)
self.otio_timeline.name = self.hiero_sequence.name()
add_tracks()
return self.otio_timeline

View file

@ -0,0 +1,529 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Daniel Flehner Heen"
__credits__ = ["Jakub Jezek", "Daniel Flehner Heen"]
import os
import hiero.core
import hiero.ui
import PySide2.QtWidgets as qw
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote # lint:ok
import opentimelineio as otio
def inform(messages):
if isinstance(messages, type('')):
messages = [messages]
qw.QMessageBox.information(
hiero.ui.mainWindow(),
'OTIO Import',
'\n'.join(messages),
qw.QMessageBox.StandardButton.Ok
)
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(otio_clip, hiero_track):
for item in hiero_track.items():
if item.timelineIn() == otio_clip.range_in_parent().start_time.value:
if item.name() == otio_clip.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, hiero_track)
if _out:
trackitem_out = find_trackitem(_out, hiero_track)
return trackitem_in, trackitem_out
def apply_transition(otio_track, otio_item, track):
warning = None
# Figure out type of transition
transition_type = get_transition_type(otio_item, otio_track)
# Figure out track kind for getattr below
kind = ''
if isinstance(track, hiero.core.AudioTrack):
kind = 'Audio'
# 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)
)
try:
transition = transition_func(
item_in,
item_out,
otio_item.in_offset.value,
otio_item.out_offset.value
)
# Catch error raised if transition is bigger than TrackItem source
except RuntimeError as e:
transition = None
warning = \
'Unable to apply transition "{t.name}": {e} ' \
'Ignoring the transition.' \
.format(t=otio_item, e=e.message)
elif transition_type == 'fade_in':
transition_func = getattr(
hiero.core.Transition,
'create{kind}FadeInTransition'.format(kind=kind)
)
# Warn user if part of fade is outside of clip
if otio_item.in_offset.value:
warning = \
'Fist half of transition "{t.name}" is outside of clip and ' \
'not valid in Hiero. Only applied second half.' \
.format(t=otio_item)
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
)
# Warn user if part of fade is outside of clip
if otio_item.out_offset.value:
warning = \
'Second half of transition "{t.name}" is outside of clip ' \
'and not valid in Hiero. Only applied first half.' \
.format(t=otio_item)
else:
# Unknown transition
return
# Apply transition to track
if transition:
track.addTransition(transition)
# Inform user about missing or adjusted transitions
return warning
def prep_url(url_in):
url = unquote(url_in)
if url.startswith('file://localhost/'):
return url
url = 'file://localhost{sep}{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
)
legal_media_refs = (
otio.schema.ExternalReference,
otio.schema.ImageSequenceReference
)
if isinstance(otio_clip.media_reference, legal_media_refs):
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, project=None, sequence=None):
otio_timeline = otio.adapters.read_from_file(otio_file)
build_sequence(otio_timeline, project=project, sequence=sequence)
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.get('Hiero', dict()).items():
if key == 'source_type':
# Only used internally to reassign tag to correct Hiero item
continue
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:
meta = marker.metadata.get('Hiero', dict())
if 'source_type' in meta:
if hiero_item.__class__.__name__ != meta.get('source_type'):
continue
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
)
if hasattr(hiero_item, 'addTagToRange'):
tag = hiero_item.addTagToRange(_tag, start, end)
else:
tag = hiero_item.addTag(_tag)
tag.setName(marker.name or marker_color_map[marker_color])
# tag.setNote(meta.get('tag.note', ''))
# Add metadata
add_metadata(marker.metadata, tag)
def create_track(otio_track, tracknum, track_kind):
if track_kind is None and hasattr(otio_track, 'kind'):
track_kind = otio_track.kind
# Create a Track
if 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, sequencebin):
# Create MediaSource
url = None
media = None
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)
elif isinstance(otio_media, otio.schema.ImageSequenceReference):
url = prep_url(otio_media.abstract_target_url('#'))
media = hiero.core.MediaSource(url)
if media is None or media.isOffline():
media = create_offline_mediasource(otio_clip, url)
# Reuse previous clip if possible
clip = None
for item in sequencebin.clips():
if item.activeItem().mediaSource() == media:
clip = item.activeItem()
break
if not clip:
# Create new Clip
clip = hiero.core.Clip(media)
# Add Clip to a Bin
sequencebin.addItem(hiero.core.BinItem(clip))
# 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)
time_scalar = 1.
# Check for speed effects and adjust playback speed accordingly
for effect in otio_clip.effects:
if isinstance(effect, otio.schema.LinearTimeWarp):
time_scalar = effect.time_scalar
# Only reverse effect can be applied here
if abs(time_scalar) == 1.:
trackitem.setPlaybackSpeed(trackitem.playbackSpeed() * time_scalar)
elif isinstance(effect, otio.schema.FreezeFrame):
# For freeze frame, playback speed must be set after range
time_scalar = 0.
# If reverse playback speed swap source in and out
if trackitem.playbackSpeed() < 0:
source_out = source_range.start_time.value
source_in = source_range.end_time_inclusive().value
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.end_time_inclusive().value
timeline_in = playhead
timeline_out = (
timeline_in +
source_range.duration.value
) - 1
# Set source and timeline in/out points
trackitem.setTimes(
timeline_in,
timeline_out,
source_in,
source_out
)
# Apply playback speed for freeze frames
if abs(time_scalar) != 1.:
trackitem.setPlaybackSpeed(trackitem.playbackSpeed() * time_scalar)
# Link audio to video when possible
if isinstance(track, hiero.core.AudioTrack):
for other in track.parent().trackItemsAt(playhead):
if other.source() == clip:
trackitem.link(other)
return trackitem
def build_sequence(otio_timeline, project=None, sequence=None, track_kind=None):
if project is None:
if sequence:
project = sequence.project()
else:
# Per version 12.1v2 there is no way of getting active project
project = hiero.core.projects(hiero.core.Project.kUserProjects)[-1]
projectbin = project.clipsBin()
if not sequence:
# Create a Sequence
sequence = hiero.core.Sequence(otio_timeline.name or 'OTIOSequence')
# Set sequence settings from otio timeline if available
if hasattr(otio_timeline, 'global_start_time'):
if otio_timeline.global_start_time:
start_time = otio_timeline.global_start_time
sequence.setFramerate(start_time.rate)
sequence.setTimecodeStart(start_time.value)
# Create a Bin to hold clips
projectbin.addItem(hiero.core.BinItem(sequence))
sequencebin = hiero.core.Bin(sequence.name())
projectbin.addItem(sequencebin)
else:
sequencebin = projectbin
# Get tagsBin
tagsbin = hiero.core.project("Tag Presets").tagsBin()
# Add timeline markers
add_markers(otio_timeline, sequence, tagsbin)
if isinstance(otio_timeline, otio.schema.Timeline):
tracks = otio_timeline.tracks
else:
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.Track, otio.schema.Stack)):
inform('Nested sequences/tracks are created separately.')
# Add gap where the nested sequence would have been
playhead += otio_clip.source_range.duration.value
# Process nested sequence
build_sequence(
otio_clip,
project=project,
track_kind=otio_track.kind
)
elif isinstance(otio_clip, otio.schema.Clip):
# Create a Clip
clip = create_clip(otio_clip, tagsbin, sequencebin)
# Create TrackItem
trackitem = create_trackitem(
playhead,
track,
otio_clip,
clip
)
# Add markers
add_markers(otio_clip, trackitem, tagsbin)
# 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
warnings = list()
for otio_track, otio_item in _transitions:
# Catch warnings form transitions in case of unsupported transitions
warning = apply_transition(otio_track, otio_item, track)
if warning:
warnings.append(warning)
if warnings:
inform(warnings)

View file

@ -1,42 +1,15 @@
# MIT License #!/usr/bin/env python
# # -*- coding: utf-8 -*-
# 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.
__author__ = "Daniel Flehner Heen"
__credits__ = ["Jakub Jezek", "Daniel Flehner Heen"]
import os import os
import re
import hiero.core import hiero.core
from hiero.core import util from hiero.core import util
import opentimelineio as otio import opentimelineio as otio
from pype.hosts.hiero.otio.hiero_export import create_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): class OTIOExportTask(hiero.core.TaskBase):
@ -44,295 +17,14 @@ class OTIOExportTask(hiero.core.TaskBase):
def __init__(self, initDict): def __init__(self, initDict):
"""Initialize""" """Initialize"""
hiero.core.TaskBase.__init__(self, initDict) hiero.core.TaskBase.__init__(self, initDict)
self.otio_timeline = None
def name(self): def name(self):
return str(type(self)) return str(type(self))
def get_rate(self, item):
if not hasattr(item, 'framerate'):
item = item.sequence()
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):
# Get rate from source or sequence
if trackitem.source().mediaSource().hasVideo():
rate_item = trackitem.source()
else:
rate_item = trackitem.sequence()
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
)
hiero_clip = trackitem.source()
available_range = None
if hiero_clip.mediaSource().isMediaPresent():
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 = r'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
)
)
metadata = dict(
Hiero=tag.metadata().dict()
)
# Store the source item for future import assignment
metadata['Hiero']['source_type'] = hiero_item.__class__.__name__
marker = otio.schema.Marker(
name=tag.name(),
color=self.get_marker_color(tag),
marked_range=marked_range,
metadata=metadata
)
otio_item.markers.append(marker)
def add_clip(self, trackitem, otio_track, itemindex):
hiero_clip = trackitem.source()
# Add Gap if needed
if itemindex == 0:
prev_item = trackitem
else:
prev_item = trackitem.parent().items()[itemindex - 1]
clip_diff = trackitem.timelineIn() - prev_item.timelineOut()
if itemindex == 0 and trackitem.timelineIn() > 0:
self.add_gap(trackitem, otio_track, 0)
elif itemindex and clip_diff != 1:
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(
name=trackitem.name(),
source_range=source_range
)
# Add media reference
media_reference = otio.schema.MissingReference()
if hiero_clip.mediaSource().isMediaPresent():
source = hiero_clip.mediaSource()
first_file = source.fileinfos()[0]
path = first_file.filename()
if "%" in path:
path = re.sub(r"%\d+d", "%d", path)
if "#" in path:
path = re.sub(r"#+", "%d", path)
media_reference = otio.schema.ExternalReference(
target_url=u'{}'.format(path),
available_range=available_range
)
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, otio_clip)
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
)
if alignment == 'kFadeIn':
otio_track.insert(-1, 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(name=track.name(), kind=kind)
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()
# Set global start time based on sequence
self.otio_timeline.global_start_time = otio.opentime.RationalTime(
self._sequence.timecodeStart(),
self._sequence.framerate().toFloat()
)
self.otio_timeline.name = self._sequence.name()
self.add_tracks()
def startTask(self): def startTask(self):
self.create_OTIO() self.otio_timeline = create_OTIO(
self._sequence)
def taskStep(self): def taskStep(self):
return False return False

View file

@ -1,3 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Daniel Flehner Heen"
__credits__ = ["Jakub Jezek", "Daniel Flehner Heen"]
import hiero.ui import hiero.ui
import OTIOExportTask import OTIOExportTask
@ -14,6 +20,8 @@ except ImportError:
FormLayout = QFormLayout # lint:ok FormLayout = QFormLayout # lint:ok
from pype.hosts.hiero.otio import hiero_export
class OTIOExportUI(hiero.ui.TaskUIBase): class OTIOExportUI(hiero.ui.TaskUIBase):
def __init__(self, preset): def __init__(self, preset):
@ -27,7 +35,7 @@ class OTIOExportUI(hiero.ui.TaskUIBase):
def includeMarkersCheckboxChanged(self, state): def includeMarkersCheckboxChanged(self, state):
# Slot to handle change of checkbox state # Slot to handle change of checkbox state
self._preset.properties()["includeTags"] = state == QtCore.Qt.Checked hiero_export.hiero_sequence = state == QtCore.Qt.Checked
def populateUI(self, widget, exportTemplate): def populateUI(self, widget, exportTemplate):
layout = widget.layout() layout = widget.layout()

View file

@ -1,25 +1,3 @@
# 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 OTIOExportTask import OTIOExportTask
from OTIOExportUI import OTIOExportUI from OTIOExportUI import OTIOExportUI

View file

@ -1,42 +1,91 @@
# MIT License #!/usr/bin/env python
# # -*- coding: utf-8 -*-
# Copyright (c) 2018 Daniel Flehner Heen (Storm Studios)
# __author__ = "Daniel Flehner Heen"
# Permission is hereby granted, free of charge, to any person obtaining a copy __credits__ = ["Jakub Jezek", "Daniel Flehner Heen"]
# 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.ui
import hiero.core import hiero.core
from otioimporter.OTIOImport import load_otio import PySide2.QtWidgets as qw
from pype.hosts.hiero.otio.hiero_import import load_otio
class OTIOProjectSelect(qw.QDialog):
def __init__(self, projects, *args, **kwargs):
super(OTIOProjectSelect, self).__init__(*args, **kwargs)
self.setWindowTitle('Please select active project')
self.layout = qw.QVBoxLayout()
self.label = qw.QLabel(
'Unable to determine which project to import sequence to.\n'
'Please select one.'
)
self.layout.addWidget(self.label)
self.projects = qw.QComboBox()
self.projects.addItems(map(lambda p: p.name(), projects))
self.layout.addWidget(self.projects)
QBtn = qw.QDialogButtonBox.Ok | qw.QDialogButtonBox.Cancel
self.buttonBox = qw.QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def get_sequence(view):
sequence = None
if isinstance(view, hiero.ui.TimelineEditor):
sequence = view.sequence()
elif isinstance(view, hiero.ui.BinView):
for item in view.selection():
if not hasattr(item, 'acitveItem'):
continue
if isinstance(item.activeItem(), hiero.core.Sequence):
sequence = item.activeItem()
return sequence
def OTIO_menu_action(event): def OTIO_menu_action(event):
otio_action = hiero.ui.createMenuAction( # Menu actions
'Import OTIO', otio_import_action = hiero.ui.createMenuAction(
'Import OTIO...',
open_otio_file, open_otio_file,
icon=None icon=None
) )
hiero.ui.registerAction(otio_action)
otio_add_track_action = hiero.ui.createMenuAction(
'New Track(s) from OTIO...',
open_otio_file,
icon=None
)
otio_add_track_action.setEnabled(False)
hiero.ui.registerAction(otio_import_action)
hiero.ui.registerAction(otio_add_track_action)
view = hiero.ui.currentContextMenuView()
if view:
sequence = get_sequence(view)
if sequence:
otio_add_track_action.setEnabled(True)
for action in event.menu.actions(): for action in event.menu.actions():
if action.text() == 'Import': if action.text() == 'Import':
action.menu().addAction(otio_action) action.menu().addAction(otio_import_action)
break action.menu().addAction(otio_add_track_action)
elif action.text() == 'New Track':
action.menu().addAction(otio_add_track_action)
def open_otio_file(): def open_otio_file():
@ -45,8 +94,39 @@ def open_otio_file():
pattern='*.otio', pattern='*.otio',
requiredExtension='.otio' requiredExtension='.otio'
) )
selection = None
sequence = None
view = hiero.ui.currentContextMenuView()
if view:
sequence = get_sequence(view)
selection = view.selection()
if sequence:
project = sequence.project()
elif selection:
project = selection[0].project()
elif len(hiero.core.projects()) > 1:
dialog = OTIOProjectSelect(hiero.core.projects())
if dialog.exec_():
project = hiero.core.projects()[dialog.projects.currentIndex()]
else:
bar = hiero.ui.mainWindow().statusBar()
bar.showMessage(
'OTIO Import aborted by user',
timeout=3000
)
return
else:
project = hiero.core.projects()[-1]
for otio_file in files: for otio_file in files:
load_otio(otio_file) load_otio(otio_file, project, sequence)
# HieroPlayer is quite limited and can't create transitions etc. # HieroPlayer is quite limited and can't create transitions etc.
@ -55,3 +135,7 @@ if not hiero.core.isHieroPlayer():
"kShowContextMenu/kBin", "kShowContextMenu/kBin",
OTIO_menu_action OTIO_menu_action
) )
hiero.core.events.registerInterest(
"kShowContextMenu/kTimeline",
OTIO_menu_action
)

1
test_localsystem.txt Normal file
View file

@ -0,0 +1 @@
I have run