Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve: update to new publisher #157

Closed
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f7e514a
Updating Resolve API reference
jakubjezek001 Mar 7, 2024
f943fe7
Add Ayon core constants and menu changes, update AyonMenu class, and …
jakubjezek001 Mar 7, 2024
09d7dc6
Update Resolve API functions and markers for Ayon compatibility.
jakubjezek001 Mar 7, 2024
fbd0fc5
Update plugin classes and imports for backward compatibility with new…
jakubjezek001 Mar 7, 2024
b692cde
Add import statement, update class inheritance, and define new attrib…
jakubjezek001 Mar 7, 2024
f634f19
Add workfile auto-creator plugin with instance creation logic.
jakubjezek001 Mar 7, 2024
666c76a
Update timeline frame rate settings and add otio metadata to Resolve …
jakubjezek001 Mar 7, 2024
6c42394
Refactor timeline item retrieval functions and menu launchers
jakubjezek001 Mar 7, 2024
18ccc00
Update Resolve API functions, containerization methods, and marker ha…
jakubjezek001 Mar 7, 2024
cabf6be
Remove deprecated functions and use constants for clip color in Resol…
jakubjezek001 Mar 7, 2024
bb5a23d
Merge branch 'develop' into feature/AY-979_Resolve-update-to-new-publ…
jakubjezek001 Mar 8, 2024
5517438
fixing bits after renaming and moving into constants
jakubjezek001 Mar 12, 2024
0c84f6f
adding function for getting native otio file
jakubjezek001 Mar 12, 2024
2a64d72
adding HostContext class for dealing with context in creators
jakubjezek001 Mar 12, 2024
0d90f57
adding abstracted plugins for dealing with new publisher
jakubjezek001 Mar 12, 2024
d03c2bf
create otio timeline can optionally add own timeline
jakubjezek001 Mar 12, 2024
17cfa43
Merge branch 'develop' into feature/AY-979_Resolve-update-to-new-publ…
jakubjezek001 Jun 12, 2024
6e1851f
Refactor menu stylesheet loading, update plugin creation logic
jakubjezek001 Jun 12, 2024
553602e
Merge branch 'develop' into feature/AY-979_Resolve-update-to-new-publ…
jakubjezek001 Jun 12, 2024
cdc55ed
Update API documentation and keyframe mode information. Remove outdat…
jakubjezek001 Jun 13, 2024
e1dea1c
Refactor otio file handling functions, add temp dir logic.
jakubjezek001 Jun 13, 2024
e4a11ef
Refactor get_otio_temp_dir to handle missing timeline.
jakubjezek001 Jun 13, 2024
932dfd6
Merge branch 'develop' into feature/AY-979_Resolve-update-to-new-publ…
jakubjezek001 Jun 13, 2024
3d1f0d7
reverting latest commit changes partly
jakubjezek001 Jun 13, 2024
d105d13
Update server_addon/resolve/client/ayon_resolve/api/lib.py
jakubjezek001 Jun 14, 2024
4ea3887
Merge branch 'develop' into feature/AY-979_Resolve-update-to-new-publ…
jakubjezek001 Jun 14, 2024
cd003ba
Merge branch 'develop' into feature/AY-979_Resolve-update-to-new-publ…
iLLiCiTiT Jun 20, 2024
210d23d
Merge branch 'develop' into feature/AY-979_Resolve-update-to-new-publ…
iLLiCiTiT Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions client/ayon_core/hosts/resolve/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
import re
import os
import contextlib
import tempfile
from opentimelineio import opentime

from ayon_core.lib import Logger
from ayon_core.pipeline.editorial import (
is_overlapping_otio_ranges,
frames_to_timecode
)
from ayon_core.pipeline.tempdir import create_custom_tempdir

from . import constants
from ..otio import davinci_export as otio_export

Expand Down Expand Up @@ -932,6 +935,46 @@ def get_otio_clip_instance_data(otio_timeline, timeline_item_data):
return None


def get_timeline_otio_filepath(project_name, anatomy=None, timeline=None):
"""Get timeline otio filepath.
jakubjezek001 marked this conversation as resolved.
Show resolved Hide resolved

Args:
project_name (str): ayon project name
anatomy (ayon_core.pipeline.Anatomy)[optional]: Anatomy object
timeline (resolve.Timeline)[optional]: resolve's object

Returns:
str: temporary otio filepath
"""
from . import bmdvr
resolve_project = get_current_resolve_project()
timeline = resolve_project.GetCurrentTimeline()
timeline_name = timeline.GetName()
jakubjezek001 marked this conversation as resolved.
Show resolved Hide resolved

# get custom staging dir
custom_temp_dir = create_custom_tempdir(project_name, anatomy)
staging_dir = os.path.normpath(
tempfile.mkdtemp(
prefix="resolve_otio_tmp_",
dir=custom_temp_dir
)
)
filename = os.path.join(staging_dir, f"{timeline_name}.otio")

# Native otio export is available from Resolve 18.5
# [major, minor, patch, build, suffix]
resolve_version = bmdvr.GetVersion()
if resolve_version[0] < 18 or resolve_version[1] < 5:
# if it is lower then use ayon's otio exporter
otio_timeline = otio_export.create_otio_timeline(
resolve_project, timeline=timeline)
otio_export.write_to_file(otio_timeline, filename)

timeline.Export(filename, bmdvr.EXPORT_OTIO)

return filename


def get_reformated_path(path, padded=False, first=False):
"""
Return fixed python expression path
Expand Down
138 changes: 138 additions & 0 deletions client/ayon_core/hosts/resolve/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"""
import os
import contextlib
import atexit
import tempfile
import json
from collections import OrderedDict

from pyblish import api as pyblish
Expand Down Expand Up @@ -260,3 +263,138 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
# Whether instances should be passthrough based on new value
timeline_item = instance.data["item"]
set_publish_attribute(timeline_item, new_value)


class HostContext:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a docstring about what this does and what it tries to solve?

It seems overly complex, trying to manage 'data' that should really be INSIDE the project. Why do we need this global cache that also gets removed afterwards. What is this 'context'? Why does it contain 'instances' if it's not workfile related?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure where should be this comment on. It seems the code had changed a lot.

Copy link
Collaborator

@BigRoy BigRoy Jun 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be on the right spot still. Basically, what is the HostContext class and why do we need it? Would be good to have a docstring to describe its usage. Why couldn't we store this data within the project?

_context_json_path = None

@staticmethod
def _on_exit():
if (
HostContext._context_json_path
and os.path.exists(HostContext._context_json_path)
):
os.remove(HostContext._context_json_path)

@classmethod
def get_context_json_path(cls):
if cls._context_json_path is None:
output_file = tempfile.NamedTemporaryFile(
mode="w", prefix="resolve_", suffix=".json"
)
output_file.close()
cls._context_json_path = output_file.name
atexit.register(HostContext._on_exit)
print(cls._context_json_path)
return cls._context_json_path

@classmethod
def _get_data(cls, group=None):
json_path = cls.get_context_json_path()
data = {}
if not os.path.exists(json_path):
with open(json_path, "w") as json_stream:
json.dump(data, json_stream)
else:
with open(json_path, "r") as json_stream:
content = json_stream.read()
if content:
data = json.loads(content)
if group is None:
return data
return data.get(group)

@classmethod
def _save_data(cls, group, new_data):
json_path = cls.get_context_json_path()
data = cls._get_data()
data[group] = new_data
with open(json_path, "w") as json_stream:
json.dump(data, json_stream)

@classmethod
def add_instance(cls, instance):
instances = cls.get_instances()
instances.append(instance)
cls.save_instances(instances)

@classmethod
def get_instances(cls):
return cls._get_data("instances") or []

@classmethod
def save_instances(cls, instances):
cls._save_data("instances", instances)

@classmethod
def get_context_data(cls):
return cls._get_data("context") or {}

@classmethod
def save_context_data(cls, data):
cls._save_data("context", data)

@classmethod
def get_project_name(cls):
return cls._get_data("project_name")

@classmethod
def set_project_name(cls, project_name):
cls._save_data("project_name", project_name)

@classmethod
def get_data_to_store(cls):
return {
"project_name": cls.get_project_name(),
"instances": cls.get_instances(),
"context": cls.get_context_data(),
}


def list_instances():
return HostContext.get_instances()


def update_instances(update_list):
updated_instances = {}
for instance, _changes in update_list:
updated_instances[instance.id] = instance.data_to_store()

instances = HostContext.get_instances()
for instance_data in instances:
instance_id = instance_data["instance_id"]
if instance_id in updated_instances:
new_instance_data = updated_instances[instance_id]
old_keys = set(instance_data.keys())
new_keys = set(new_instance_data.keys())
instance_data.update(new_instance_data)
for key in (old_keys - new_keys):
instance_data.pop(key)

HostContext.save_instances(instances)


def remove_instances(instances):
if not isinstance(instances, (tuple, list)):
instances = [instances]

current_instances = HostContext.get_instances()
for instance in instances:
instance_id = instance.data["instance_id"]
found_idx = None
for idx, _instance in enumerate(current_instances):
if instance_id == _instance["instance_id"]:
found_idx = idx
break

if found_idx is not None:
current_instances.pop(found_idx)
HostContext.save_instances(current_instances)


def get_context_data():
return HostContext.get_context_data()


def update_context_data(data, changes):
HostContext.save_context_data(data)
92 changes: 92 additions & 0 deletions client/ayon_core/hosts/resolve/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,26 @@
Creator as NewCreator
)

from ayon_core.pipeline.create import (
Creator,
HiddenCreator,
CreatedInstance,
cache_and_get_instances,
)

from .pipeline import (
list_instances,
update_instances,
remove_instances,
HostContext,
)

from . import lib, constants


SHARED_DATA_KEY = "ayon.resolve.instances"


class ClipLoader:

active_bin = None
Expand Down Expand Up @@ -653,3 +670,78 @@ def _create_parents(self):

# alias for backward compatibility
PublishClip = PublishableClip # noqa


class HiddenResolvePublishCreator(HiddenCreator):
host_name = "resolve"
settings_category = "resolve"

def collect_instances(self):
instances_by_identifier = cache_and_get_instances(
self, SHARED_DATA_KEY, list_instances
)
for instance_data in instances_by_identifier[self.identifier]:
instance = CreatedInstance.from_existing(instance_data, self)
self._add_instance_to_context(instance)

def update_instances(self, update_list):
update_instances(update_list)

def remove_instances(self, instances):
remove_instances(instances)
for instance in instances:
self._remove_instance_from_context(instance)

def _store_new_instance(self, new_instance):
"""Resolve publisher specific method to store instance.

Instance is stored into "workfile" of Resolve and also add it
to CreateContext.

Args:
new_instance (CreatedInstance): Instance that should be stored.
"""

# Host implementation of storing metadata about instance
HostContext.add_instance(new_instance.data_to_store())
# Add instance to current context
self._add_instance_to_context(new_instance)


class ResolvePublishCreator(Creator):
create_allow_context_change = True
host_name = "resolve"
settings_category = "resolve"

def collect_instances(self):
instances_by_identifier = cache_and_get_instances(
self, SHARED_DATA_KEY, list_instances
)
for instance_data in instances_by_identifier[self.identifier]:
instance = CreatedInstance.from_existing(instance_data, self)
self._add_instance_to_context(instance)

def update_instances(self, update_list):
update_instances(update_list)

def remove_instances(self, instances):
remove_instances(instances)
for instance in instances:
self._remove_instance_from_context(instance)

def _store_new_instance(self, new_instance):
"""Resolve publisher specific method to store instance.

Instance is stored into "workfile" of Resolve and also add it
to CreateContext.

Args:
new_instance (CreatedInstance): Instance that should be stored.
"""

# Host implementation of storing metadata about instance
HostContext.add_instance(new_instance.data_to_store())
new_instance.mark_as_stored()

# Add instance to current context
self._add_instance_to_context(new_instance)
4 changes: 2 additions & 2 deletions client/ayon_core/hosts/resolve/otio/davinci_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,10 @@ def add_otio_metadata(otio_item, media_pool_item, **kwargs):
otio_item.metadata.update({key: value})


def create_otio_timeline(resolve_project):
def create_otio_timeline(resolve_project, timeline=None):

# get current timeline
timeline = resolve_project.GetCurrentTimeline()
timeline = timeline or resolve_project.GetCurrentTimeline()
self.project_fps = timeline.GetSetting("timelineFrameRate")

# convert timeline to otio
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,6 @@ def remove_instances(self, instances):

# removing instance by marker color
print(f"Removing instance: {track_item.GetName()}")
track_item.DeleteMarkersByColor(lib.pype_marker_color)
track_item.DeleteMarkersByColor(constants.ayon_marker_color)

self._remove_instance_from_context(instance)
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class CollectResolveProject(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder - 0.499

def process(self, context):
resolve_project = rapi.get_current_project()
resolve_project = rapi.get_current_resolve_project()
timeline = resolve_project.GetCurrentTimeline()
fps = timeline.GetSetting("timelineFrameRate")

Expand Down