Consolidate frame range detection from collect_otio_frame_ranges plugin.

This commit is contained in:
robin@ynput.io 2025-02-11 10:39:35 +01:00
parent 971c4aef43
commit 6bb4937c0a
3 changed files with 2234 additions and 23 deletions

View file

@ -101,8 +101,7 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
tl_start_h, tl_end_h = otio_range_to_frame_range(otio_tl_range_handles)
frame_start = workfile_start
frame_end = frame_start + otio.opentime.to_frames(
otio_tl_range.duration, otio_tl_range.duration.rate) - 1
frame_end = frame_start + otio_tl_range.duration.to_frames() - 1
data = {
"frameStart": frame_start,
@ -120,39 +119,68 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
# 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
# Backward-compatibility for Hiero OTIO exporter.
# NTSC compatibility might introduce floating rates, when these are
# not exactly the same (23.976 vs 23.976024627685547)
# this will cause precision issue in computation.
# Currently round to 2 decimals for comparison,
# but this should always rescale after that.
rounded_av_rate = round(otio_available_range.start_time.rate, 2)
rounded_src_rate = round(otio_src_range.start_time.rate, 2)
if rounded_av_rate != rounded_src_rate:
conformed_src_in = otio_src_range.start_time.rescaled_to(
otio_available_range.start_time.rate
)
conformed_src_duration = otio_src_range.duration.rescaled_to(
otio_available_range.duration.rate
)
conformed_source_range = otio.opentime.TimeRange(
start_time=conformed_src_in,
duration=conformed_src_duration
)
else:
conformed_source_range = otio_src_range
source_start = conformed_source_range.start_time
source_end = source_start + conformed_source_range.duration
handle_start = otio.opentime.RationalTime(
instance.data.get("handleStart", 0),
source_start.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)
handle_end = otio.opentime.RationalTime(
instance.data.get("handleEnd", 0),
source_start.rate
)
source_start_h = source_start - handle_start
source_end_h = source_end + handle_end
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,
"sourceStart": source_start.to_frames(),
"sourceEnd": source_end.to_frames() - 1,
"sourceStartH": source_start_h.to_frames(),
"sourceEndH": source_end_h.to_frames() - 1,
}
instance.data.update(data)
self.log.debug(f"Added source ranges: {pformat(data)}")
def _collect_retimed_ranges(self, instance, otio_clip):
"""Handle retimed clip frame ranges."""
workfile_source_duration = instance.data.get("shotDurationFromSource")
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}")
frame_start = instance.data["frameStart"]
media_in = int(retimed_attributes["mediaIn"])
media_out = int(retimed_attributes["mediaOut"])
frame_end = frame_start + (media_out - media_in) + 1
frame_end = frame_start + (media_out - media_in)
instance.data["frameEnd"] = frame_end
self.log.debug(f"Updated frameEnd for retimed clip: {frame_end}")
data = {
"frameStart": frame_start,
"frameEnd": frame_end,
"sourceStart": media_in,
"sourceEnd": media_out,
"sourceStartH": media_in - int(retimed_attributes["handleStart"]),
"sourceEndH": media_out + int(retimed_attributes["handleEnd"]),
}
instance.data.update(data)
self.log.debug(f"Updated retimed values: {data}")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,129 @@
import os
import mock
import opentimelineio as otio
from ayon_core.plugins.publish import collect_otio_frame_ranges
_RESOURCE_DIR = os.path.join(
os.path.dirname(__file__),
"resources",
"timeline"
)
class MockInstance():
""" Mock pyblish instance for testing purpose.
"""
def __init__(self, data: dict):
self.data = data
self.context = self
def _check_expected_frame_range_values(
clip_name: str,
expected_data: dict,
handle_start: int = 10,
handle_end: int = 10,
retimed: bool = False,
):
file_path = os.path.join(_RESOURCE_DIR, "timeline.json")
otio_timeline = otio.schema.Timeline.from_json_file(file_path)
for otio_clip in otio_timeline.find_clips():
if otio_clip.name == clip_name:
break
instance_data = {
"otioClip": otio_clip,
"handleStart": handle_start,
"handleEnd": handle_end,
"workfileFrameStart": 1001,
}
if retimed:
instance_data["shotDurationFromSource"] = True
instance = MockInstance(instance_data)
processor = collect_otio_frame_ranges.CollectOtioRanges()
processor.process(instance)
# Assert expected data is subset of edited instance.
assert expected_data.items() <= instance.data.items()
def test_movie_with_timecode():
"""
Movie clip (with embedded timecode)
available_range = 86531-86590 23.976fps
source_range = 86535-86586 23.976fps
"""
expected_data = {
'frameStart': 1001,
'frameEnd': 1052,
'clipIn': 24,
'clipOut': 75,
'clipInH': 14,
'clipOutH': 85,
'sourceStart': 86535,
'sourceStartH': 86525,
'sourceEnd': 86586,
'sourceEndH': 86596,
}
_check_expected_frame_range_values(
"sh010",
expected_data,
)
def test_image_sequence():
"""
EXR image sequence.
available_range = 87399-87482 24fps
source_range = 87311-87336 23.976fps
"""
expected_data = {
'frameStart': 1001,
'frameEnd': 1026,
'clipIn': 76,
'clipOut': 101,
'clipInH': 66,
'clipOutH': 111,
'sourceStart': 87399,
'sourceStartH': 87389,
'sourceEnd': 87424,
'sourceEndH': 87434,
}
_check_expected_frame_range_values(
"img_sequence_exr",
expected_data,
)
def test_media_retimed():
"""
EXR image sequence.
available_range = 345619-345691 23.976fps
source_range = 345623-345687 23.976fps
TimeWarp = frozen frame.
"""
expected_data = {
'frameStart': 1001,
'frameEnd': 1065,
'clipIn': 127,
'clipOut': 191,
'clipInH': 117,
'clipOutH': 201,
'sourceStart': 1001,
'sourceStartH': 1001,
'sourceEnd': 1065,
'sourceEndH': 1065,
}
_check_expected_frame_range_values(
"P01default_twsh010",
expected_data,
retimed=True,
)