diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py index 45aa5502cc..b6c43a58c2 100644 --- a/pype/hosts/resolve/__init__.py +++ b/pype/hosts/resolve/__init__.py @@ -30,7 +30,8 @@ from .lib import ( swap_clips, get_pype_clip_metadata, set_project_manager_to_folder_name, - get_reformated_path + get_reformated_path, + get_otio_clip_instance_data ) from .menu import launch_pype_menu @@ -49,12 +50,6 @@ from .workio import ( work_root ) -from .otio import ( - get_otio_clip_instance_data, - get_otio_complete_timeline, - save_otio -) - bmdvr = None bmdvf = None @@ -91,6 +86,7 @@ __all__ = [ "get_pype_clip_metadata", "set_project_manager_to_folder_name", "get_reformated_path", + "get_otio_clip_instance_data", # menu "launch_pype_menu", @@ -109,10 +105,5 @@ __all__ = [ # singleton with black magic resolve module "bmdvr", - "bmdvf", - - # open color io integration - "get_otio_clip_instance_data", - "get_otio_complete_timeline", - "save_otio" + "bmdvf" ] diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py index 777cae0eb2..6b44f97172 100644 --- a/pype/hosts/resolve/lib.py +++ b/pype/hosts/resolve/lib.py @@ -647,3 +647,36 @@ def get_reformated_path(path, padded=True): else: path = re.sub(num_pattern, f"%d", path) return path + + +def get_otio_clip_instance_data(track_item_data): + """ + Return otio objects for timeline, track and clip + + Args: + track_item_data (dict): track_item_data from list returned by + resolve.get_current_track_items() + + Returns: + dict: otio clip with parent objects + + """ + from .otio import davinci_export as otio_export + + track_item = track_item_data["clip"]["item"] + project = track_item_data["project"] + + frame_start = track_item.GetStart() + frame_duration = track_item.GetDuration() + self.project_fps = project.GetSetting("timelineFrameRate") + + otio_clip_range = otio_export.create_otio_time_range( + frame_start, frame_duration, self.project_fps) + + # create otio clip and add it to track + otio_clip = otio_export.create_otio_clip(track_item) + + return { + "otioClip": otio_clip, + "otioClipRange": otio_clip_range + } diff --git a/pype/hosts/resolve/otio/__init__.py b/pype/hosts/resolve/otio/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/hosts/resolve/otio.py b/pype/hosts/resolve/otio/davinci_export.py similarity index 72% rename from pype/hosts/resolve/otio.py rename to pype/hosts/resolve/otio/davinci_export.py index acb669196f..25c578e0a7 100644 --- a/pype/hosts/resolve/otio.py +++ b/pype/hosts/resolve/otio/davinci_export.py @@ -1,8 +1,7 @@ import sys import json import opentimelineio as otio -from . import lib - +from . import utils self = sys.modules[__name__] self.track_types = { @@ -12,17 +11,6 @@ self.track_types = { self.project_fps = None -def timecode_to_frames(timecode, framerate): - parts = zip(( - 3600 * framerate, - 60 * framerate, - framerate, 1 - ), timecode.split(":")) - return sum( - f * int(t) for f, t in parts - ) - - def create_otio_rational_time(frame, fps): return otio.opentime.RationalTime( float(frame), @@ -40,7 +28,7 @@ def create_otio_time_range(start_frame, frame_duration, fps): def create_otio_reference(media_pool_item): mp_clip_property = media_pool_item.GetClipProperty() path = mp_clip_property["File Path"] - reformat_path = lib.get_reformated_path(path, padded=False) + reformat_path = utils.get_reformated_path(path, padded=False) # get clip property regarding to type mp_clip_property = media_pool_item.GetClipProperty() @@ -51,7 +39,7 @@ def create_otio_reference(media_pool_item): else: audio_duration = str(mp_clip_property["Duration"]) frame_start = 0 - frame_duration = int(timecode_to_frames( + frame_duration = int(utils.timecode_to_frames( audio_duration, float(fps))) return otio.schema.ExternalReference( @@ -97,7 +85,7 @@ def create_otio_clip(track_item): else: fps = self.project_fps - name = lib.get_reformated_path(track_item.GetName()) + name = utils.get_reformated_path(track_item.GetName()) media_reference = create_otio_reference(media_pool_item) source_range = create_otio_time_range( @@ -141,7 +129,7 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): ) -def create_otio_timeline(timeline, fps): +def _create_otio_timeline(timeline, fps): start_time = create_otio_rational_time( timeline.GetStartFrame(), fps) otio_timeline = otio.schema.Timeline( @@ -172,13 +160,12 @@ def add_otio_gap(clip_start, otio_track, track_item, timeline): ) -def get_otio_complete_timeline(project): +def create_otio_timeline(timeline, fps): # get current timeline - timeline = project.GetCurrentTimeline() - self.project_fps = project.GetSetting("timelineFrameRate") + self.project_fps = fps # convert timeline to otio - otio_timeline = create_otio_timeline(timeline, self.project_fps) + otio_timeline = _create_otio_timeline(timeline, self.project_fps) # loop all defined track types for track_type in list(self.track_types.keys()): @@ -236,66 +223,5 @@ def get_otio_complete_timeline(project): return otio_timeline -def get_otio_clip_instance_data(track_item_data): - """ - Return otio objects for timeline, track and clip - - Args: - track_item_data (dict): track_item_data from list returned by - resolve.get_current_track_items() - - Returns: - dict: otio clip with parent objects - - """ - - track_item = track_item_data["clip"]["item"] - project = track_item_data["project"] - timeline = track_item_data["sequence"] - track_type = track_item_data["track"]["type"] - track_name = track_item_data["track"]["name"] - track_index = track_item_data["track"]["index"] - - timeline_start = timeline.GetStartFrame() - frame_start = track_item.GetStart() - frame_duration = track_item.GetDuration() - self.project_fps = project.GetSetting("timelineFrameRate") - - otio_clip_range = create_otio_time_range( - frame_start, frame_duration, self.project_fps) - # convert timeline to otio - otio_timeline = create_otio_timeline(timeline, self.project_fps) - # convert track to otio - otio_track = create_otio_track( - track_type, "{}{}".format(track_name, track_index)) - - # add gap if track item is not starting from timeline start - # if gap between track start and clip start - if frame_start > timeline_start: - # create gap and add it to track - otio_track.append( - create_otio_gap( - 0, - frame_start, - timeline_start, - self.project_fps - ) - ) - - # create otio clip and add it to track - otio_clip = create_otio_clip(track_item) - otio_track.append(otio_clip) - - # add track to otio timeline - otio_timeline.tracks.append(otio_track) - - return { - "otioTimeline": otio_timeline, - "otioTrack": otio_track, - "otioClip": otio_clip, - "otioClipRange": otio_clip_range - } - - -def save_otio(otio_timeline, path): +def write_to_file(otio_timeline, path): otio.adapters.write_to_file(otio_timeline, path) diff --git a/pype/hosts/resolve/otio/davinci_import.py b/pype/hosts/resolve/otio/davinci_import.py new file mode 100644 index 0000000000..19133279bb --- /dev/null +++ b/pype/hosts/resolve/otio/davinci_import.py @@ -0,0 +1,48 @@ +import sys +import DaVinciResolveScript +import opentimelineio as otio + + +self = sys.modules[__name__] +self.resolve = DaVinciResolveScript.scriptapp('Resolve') +self.fusion = DaVinciResolveScript.scriptapp('Fusion') +self.project_manager = self.resolve.GetProjectManager() +self.current_project = self.project_manager.GetCurrentProject() +self.media_pool = self.current_project.GetMediaPool() +self.track_types = { + "video": otio.schema.TrackKind.Video, + "audio": otio.schema.TrackKind.Audio +} +self.project_fps = None + + +def build_timeline(otio_timeline): + for clip in otio_timeline.each_clip(): + print(clip.name) + print(clip.parent().name) + print(clip.range_in_parent()) + + +def _build_track(otio_track): + pass + + +def _build_media_pool_item(otio_media_reference): + pass + + +def _build_track_item(otio_clip): + pass + + +def _build_gap(otio_clip): + pass + + +def _build_marker(otio_marker): + pass + + +def read_from_file(otio_file): + otio_timeline = otio.adapters.read_from_file(otio_file) + build_timeline(otio_timeline) diff --git a/pype/hosts/resolve/otio/utils.py b/pype/hosts/resolve/otio/utils.py new file mode 100644 index 0000000000..22619d4172 --- /dev/null +++ b/pype/hosts/resolve/otio/utils.py @@ -0,0 +1,37 @@ +import re + + +def timecode_to_frames(timecode, framerate): + parts = zip(( + 3600 * framerate, + 60 * framerate, + framerate, 1 + ), timecode.split(":")) + return sum( + f * int(t) for f, t in parts + ) + + +def get_reformated_path(path, padded=True): + """ + Return fixed python expression path + + Args: + path (str): path url or simple file name + + Returns: + type: string with reformated path + + Example: + get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr + + """ + num_pattern = "(\\[\\d+\\-\\d+\\])" + padding_pattern = "(\\d+)(?=-)" + if "[" in path: + padding = len(re.findall(padding_pattern, path).pop()) + if padded: + path = re.sub(num_pattern, f"%0{padding}d", path) + else: + path = re.sub(num_pattern, f"%d", path) + return path diff --git a/pype/hosts/resolve/utility_scripts/OTIO_export.py b/pype/hosts/resolve/utility_scripts/OTIO_export.py index a0c8e80bc7..7569ba4c42 100644 --- a/pype/hosts/resolve/utility_scripts/OTIO_export.py +++ b/pype/hosts/resolve/utility_scripts/OTIO_export.py @@ -2,7 +2,7 @@ import os import sys import opentimelineio as otio -print(otio) + resolve = bmd.scriptapp("Resolve") fu = resolve.Fusion() diff --git a/pype/hosts/resolve/utility_scripts/OTIO_import.py b/pype/hosts/resolve/utility_scripts/OTIO_import.py new file mode 100644 index 0000000000..2266fd4b2b --- /dev/null +++ b/pype/hosts/resolve/utility_scripts/OTIO_import.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +import os +import sys +from pype.hosts.resolve.otio import davinci_resolve_import as otio_import + +resolve = bmd.scriptapp("Resolve") +fu = resolve.Fusion() +ui = fu.UIManager +disp = bmd.UIDispatcher(fu.UIManager) + + +title_font = ui.Font({"PixelSize": 18}) +dlg = disp.AddWindow( + { + "WindowTitle": "Import OTIO", + "ID": "OTIOwin", + "Geometry": [250, 250, 250, 100], + "Spacing": 0, + "Margin": 10 + }, + [ + ui.VGroup( + { + "Spacing": 2 + }, + [ + ui.Button( + { + "ID": "importOTIOfileButton", + "Text": "Select OTIO File Path", + "Weight": 1.25, + "ToolTip": "Choose otio file to import from", + "Flat": False + } + ), + ui.VGap(), + ui.Button( + { + "ID": "importButton", + "Text": "Import", + "Weight": 2, + "ToolTip": "Import otio to new timeline", + "Flat": False + } + ) + ] + ) + ] +) + +itm = dlg.GetItems() + + +def _close_window(event): + disp.ExitLoop() + + +def _import_button(event): + otio_import.read_from_file(itm["importOTIOfileButton"].Text) + _close_window(None) + + +def _import_file_pressed(event): + selected_path = fu.RequestFile(os.path.expanduser("~/Documents")) + itm["importOTIOfileButton"].Text = selected_path + + +dlg.On.OTIOwin.Close = _close_window +dlg.On.importOTIOfileButton.Clicked = _import_file_pressed +dlg.On.importButton.Clicked = _import_button +dlg.Show() +disp.RunLoop() +dlg.Hide() diff --git a/pype/plugins/resolve/publish/collect_workfile.py b/pype/plugins/resolve/publish/collect_workfile.py index 0bd1a24a46..9873e1ca97 100644 --- a/pype/plugins/resolve/publish/collect_workfile.py +++ b/pype/plugins/resolve/publish/collect_workfile.py @@ -1,4 +1,3 @@ -import os import pyblish.api from pype.hosts import resolve from avalon import api as avalon @@ -6,8 +5,8 @@ from pprint import pformat # dev from importlib import reload -from pype.hosts.resolve import otio -reload(otio) +from pype.hosts.resolve.otio import davinci_export +reload(davinci_export) class CollectWorkfile(pyblish.api.ContextPlugin): @@ -23,12 +22,13 @@ class CollectWorkfile(pyblish.api.ContextPlugin): project = resolve.get_current_project() fps = project.GetSetting("timelineFrameRate") - # adding otio timeline to context - otio_timeline = resolve.get_otio_complete_timeline(project) - active_sequence = resolve.get_current_sequence() video_tracks = resolve.get_video_track_names() + # adding otio timeline to context + otio_timeline = davinci_export.create_otio_timeline( + active_sequence, fps) + instance_data = { "name": "{}_{}".format(asset, subset), "asset": asset,