-
Notifications
You must be signed in to change notification settings - Fork 39
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
Refactor OTIO frame range collection #1060
base: develop
Are you sure you want to change the base?
Changes from all commits
63ed2f2
9502b55
301e9ce
8ef1d38
5d73b31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,93 +1,204 @@ | ||
"""Plugins for collecting OTIO frame ranges and related timing information. | ||
|
||
This module contains three plugins: | ||
- CollectOtioFrameRanges: Collects basic timeline frame ranges | ||
- CollectOtioSourceRanges: Collects source media frame ranges | ||
- CollectOtioRetimedRanges: Handles retimed clip frame ranges | ||
""" | ||
Requires: | ||
otioTimeline -> context data attribute | ||
review -> instance data attribute | ||
masterLayer -> instance data attribute | ||
otioClipRange -> instance data attribute | ||
""" | ||
|
||
from pprint import pformat | ||
|
||
import pyblish.api | ||
|
||
try: | ||
import opentimelineio as otio | ||
except ImportError: | ||
raise RuntimeError("OpenTimelineIO is not installed.") | ||
|
||
from ayon_core.pipeline.editorial import ( | ||
get_media_range_with_retimes, | ||
otio_range_to_frame_range, | ||
otio_range_with_handles, | ||
) | ||
|
||
|
||
def validate_otio_clip(instance, logger): | ||
"""Validate if instance has required OTIO clip data. | ||
|
||
Args: | ||
instance: The instance to validate | ||
logger: Logger object to use for debug messages | ||
|
||
Returns: | ||
bool: True if valid, False otherwise | ||
""" | ||
if not instance.data.get("otioClip"): | ||
logger.debug("Skipping collect OTIO range - no clip found.") | ||
return False | ||
return True | ||
|
||
class CollectOtioFrameRanges(pyblish.api.InstancePlugin): | ||
"""Getting otio ranges from otio_clip | ||
"""Collect basic timeline frame ranges from OTIO clip. | ||
|
||
Adding timeline and source ranges to instance data""" | ||
This plugin extracts and stores basic timeline frame ranges including | ||
handles from the OTIO clip. | ||
|
||
Requires: | ||
otioClip (otio.schema.Clip): OTIO clip object | ||
workfileFrameStart (int): Starting frame of work file | ||
|
||
Provides: | ||
frameStart (int): Start frame in timeline | ||
frameEnd (int): End frame in timeline | ||
clipIn (int): Clip in point | ||
clipOut (int): Clip out point | ||
clipInH (int): Clip in point with handles | ||
clipOutH (int): Clip out point with handles | ||
""" | ||
|
||
label = "Collect OTIO Frame Ranges" | ||
order = pyblish.api.CollectorOrder - 0.08 | ||
families = ["shot", "clip"] | ||
hosts = ["resolve", "hiero", "flame", "traypublisher"] | ||
|
||
def process(self, instance): | ||
# Not all hosts can import these modules. | ||
import opentimelineio as otio | ||
from ayon_core.pipeline.editorial import ( | ||
get_media_range_with_retimes, | ||
otio_range_to_frame_range, | ||
otio_range_with_handles | ||
) | ||
"""Process the instance to collect frame ranges. | ||
|
||
Args: | ||
instance: The instance to process | ||
""" | ||
|
||
if not instance.data.get("otioClip"): | ||
self.log.debug("Skipping collect OTIO frame range.") | ||
if not validate_otio_clip(instance, self.log): | ||
return | ||
|
||
# get basic variables | ||
otio_clip = instance.data["otioClip"] | ||
workfile_start = instance.data["workfileFrameStart"] | ||
workfile_source_duration = instance.data.get("shotDurationFromSource") | ||
|
||
# get ranges | ||
# Get timeline ranges | ||
otio_tl_range = otio_clip.range_in_parent() | ||
otio_src_range = otio_clip.source_range | ||
otio_avalable_range = otio_clip.available_range() | ||
otio_tl_range_handles = otio_range_with_handles( | ||
otio_tl_range, instance) | ||
otio_src_range_handles = otio_range_with_handles( | ||
otio_src_range, instance) | ||
otio_tl_range_handles = otio_range_with_handles(otio_tl_range, instance) | ||
|
||
# Convert to frames | ||
tl_start, tl_end = otio_range_to_frame_range(otio_tl_range) | ||
tl_start_h, tl_end_h = otio_range_to_frame_range(otio_tl_range_handles) | ||
|
||
# get source avalable start frame | ||
src_starting_from = otio.opentime.to_frames( | ||
otio_avalable_range.start_time, | ||
otio_avalable_range.start_time.rate) | ||
|
||
# convert to frames | ||
range_convert = otio_range_to_frame_range | ||
tl_start, tl_end = range_convert(otio_tl_range) | ||
tl_start_h, tl_end_h = range_convert(otio_tl_range_handles) | ||
src_start, src_end = range_convert(otio_src_range) | ||
src_start_h, src_end_h = range_convert(otio_src_range_handles) | ||
frame_start = workfile_start | ||
frame_end = frame_start + otio.opentime.to_frames( | ||
otio_tl_range.duration, otio_tl_range.duration.rate) - 1 | ||
|
||
# in case of retimed clip and frame range should not be retimed | ||
if workfile_source_duration: | ||
# get available range trimmed with processed retimes | ||
retimed_attributes = get_media_range_with_retimes( | ||
otio_clip, 0, 0) | ||
self.log.debug( | ||
">> retimed_attributes: {}".format(retimed_attributes)) | ||
media_in = int(retimed_attributes["mediaIn"]) | ||
media_out = int(retimed_attributes["mediaOut"]) | ||
frame_end = frame_start + (media_out - media_in) + 1 | ||
self.log.debug(frame_end) | ||
|
||
data = { | ||
"frameStart": frame_start, | ||
"frameEnd": frame_end, | ||
"clipIn": tl_start, | ||
"clipOut": tl_end - 1, | ||
"clipInH": tl_start_h, | ||
"clipOutH": tl_end_h - 1, | ||
} | ||
instance.data.update(data) | ||
self.log.debug(f"Added frame ranges: {pformat(data)}") | ||
|
||
|
||
class CollectOtioSourceRanges(pyblish.api.InstancePlugin): | ||
"""Collect source media frame ranges from OTIO clip. | ||
|
||
This plugin extracts and stores source media frame ranges including | ||
handles from the OTIO clip. | ||
|
||
Requires: | ||
otioClip (otio.schema.Clip): OTIO clip object | ||
|
||
Provides: | ||
sourceStart (int): Source media start frame | ||
sourceEnd (int): Source media end frame | ||
sourceStartH (int): Source media start frame with handles | ||
sourceEndH (int): Source media end frame with handles | ||
""" | ||
|
||
label = "Collect Source OTIO Frame Ranges" | ||
order = pyblish.api.CollectorOrder - 0.07 | ||
families = ["shot", "clip"] | ||
hosts = ["hiero", "flame"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we stop using product types for filtering and use different family instead? That would move responsibility of running the plugin to host integration, which makes it easily expandable to other hosts when needed, without need to change |
||
|
||
def process(self, instance): | ||
"""Process the instance to collect source frame ranges. | ||
|
||
Args: | ||
instance: The instance to process | ||
""" | ||
|
||
if not validate_otio_clip(instance, self.log): | ||
return | ||
|
||
otio_clip = instance.data["otioClip"] | ||
|
||
# Get source ranges | ||
otio_src_range = otio_clip.source_range | ||
otio_available_range = otio_clip.available_range() | ||
otio_src_range_handles = otio_range_with_handles(otio_src_range, instance) | ||
|
||
# Get source available start frame | ||
src_starting_from = otio.opentime.to_frames( | ||
otio_available_range.start_time, | ||
otio_available_range.start_time.rate | ||
) | ||
|
||
# Convert to frames | ||
src_start, src_end = otio_range_to_frame_range(otio_src_range) | ||
src_start_h, src_end_h = otio_range_to_frame_range(otio_src_range_handles) | ||
|
||
data = { | ||
"sourceStart": src_starting_from + src_start, | ||
"sourceEnd": src_starting_from + src_end - 1, | ||
"sourceStartH": src_starting_from + src_start_h, | ||
"sourceEndH": src_starting_from + src_end_h - 1, | ||
} | ||
instance.data.update(data) | ||
self.log.debug( | ||
"_ data: {}".format(pformat(data))) | ||
self.log.debug( | ||
"_ instance.data: {}".format(pformat(instance.data))) | ||
self.log.debug(f"Added source ranges: {pformat(data)}") | ||
|
||
|
||
class CollectOtioRetimedRanges(pyblish.api.InstancePlugin): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are plugins |
||
"""Update frame ranges for retimed clips. | ||
|
||
This plugin updates the frame end value for retimed clips. | ||
|
||
Requires: | ||
otioClip (otio.schema.Clip): OTIO clip object | ||
workfileFrameStart (int): Starting frame of work file | ||
shotDurationFromSource (Optional[int]): Duration from source if retimed | ||
|
||
Provides: | ||
frameEnd (int): Updated end frame for retimed clips | ||
""" | ||
|
||
label = "Collect Retimed OTIO Frame Ranges" | ||
order = pyblish.api.CollectorOrder - 0.06 | ||
families = ["shot", "clip"] | ||
hosts = ["hiero", "flame"] | ||
|
||
def process(self, instance): | ||
"""Process the instance to handle retimed clips. | ||
|
||
Args: | ||
instance: The instance to process | ||
""" | ||
if not validate_otio_clip(instance, self.log): | ||
return | ||
|
||
workfile_source_duration = instance.data.get("shotDurationFromSource") | ||
if not workfile_source_duration: | ||
self.log.debug("No source duration found, skipping retime handling.") | ||
return | ||
|
||
otio_clip = instance.data["otioClip"] | ||
frame_start = instance.data["frameStart"] | ||
|
||
# Handle retimed clip frame range | ||
retimed_attributes = get_media_range_with_retimes(otio_clip, 0, 0) | ||
self.log.debug(f"Retimed attributes: {retimed_attributes}") | ||
|
||
media_in = int(retimed_attributes["mediaIn"]) | ||
media_out = int(retimed_attributes["mediaOut"]) | ||
frame_end = frame_start + (media_out - media_in) + 1 | ||
|
||
instance.data["frameEnd"] = frame_end | ||
self.log.debug(f"Updated frameEnd for retimed clip: {frame_end}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause that the plugin won't be loaded at all. So artist even doesn't know it failed, and he's missing "maybe" important logic.
BTW if it would be kept why to raise
RuntimeError
instead of keepingImportError
without try > except?