diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index e25d3479ee..561f71eb52 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -122,6 +122,7 @@ from .transcoding import ( convert_ffprobe_fps_value, convert_ffprobe_fps_to_float, get_rescaled_command_arguments, + get_image_info_metadata, ) from .plugin_tools import ( @@ -159,15 +160,11 @@ __all__ = [ "get_local_site_id", "get_ayon_username", "get_openpype_username", - "initialize_ayon_connection", - "CacheItem", "NestedCacheItem", - "emit_event", "register_event_callback", - "get_ayon_launcher_args", "get_openpype_execute_args", "get_linux_launcher_args", @@ -178,10 +175,8 @@ __all__ = [ "run_openpype_process", "path_to_subprocess_arg", "CREATE_NO_WINDOW", - "env_value_to_bool", "get_paths_from_environ", - "ToolNotFoundError", "find_executable", "get_oiio_tools_path", @@ -189,13 +184,10 @@ __all__ = [ "get_ffmpeg_tool_path", "get_ffmpeg_tool_args", "is_oiio_supported", - "AbstractAttrDef", - "UIDef", "UISeparatorDef", "UILabelDef", - "UnknownDef", "NumberDef", "TextDef", @@ -203,14 +195,12 @@ __all__ = [ "BoolDef", "FileDef", "FileDefItem", - "import_filepath", "modules_from_path", "recursive_bases_from_class", "classes_from_module", "import_module_from_dirpath", "is_func_signature_supported", - "get_transcode_temp_directory", "should_convert_for_ffmpeg", "convert_for_ffmpeg", @@ -222,33 +212,26 @@ __all__ = [ "convert_ffprobe_fps_value", "convert_ffprobe_fps_to_float", "get_rescaled_command_arguments", + "get_image_info_metadata", "compile_list_of_regexes", - "filter_profiles", - "prepare_template_data", "source_hash", - "format_file_size", "collect_frames", "create_hard_link", "version_up", "get_version_from_path", "get_last_version_from_path", - "TemplateUnsolved", "StringTemplate", "FormatObject", - "terminal", - "get_datetime_data", "get_timestamp", "get_formatted_current_time", - "Logger", - "is_in_ayon_launcher_process", "is_running_from_build", "is_using_ayon_console", diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 4d778c2091..0f220f1093 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -834,6 +834,90 @@ def get_ffprobe_streams(path_to_file, logger=None): return get_ffprobe_data(path_to_file, logger)["streams"] +def get_image_info_metadata( + path_to_file, + keys=None, + logger=None +): + """Get metadata from image file. + + At first it will try to detect if the image input is supported by + OpenImageIO. If it is then it gets the metadata from the image using + OpenImageIO. If it is not supported by OpenImageIO then it will try to + get the metadata using FFprobe. + + Args: + path_to_file (str): Path to image file. + keys (list[str]): List of keys that should be returned. If None then + all keys are returned. + logger (logging.Logger): Logger used for logging. + """ + if logger is None: + logger = logging.getLogger(__name__) + + def _ffprobe_metadata_conversion(metadata): + """Convert ffprobe metadata unified format.""" + output = {} + for key, val in metadata.items(): + if key in ("tags", "disposition"): + output.update(val) + else: + output[key] = val + return output + + metadata_stream = None + ext = os.path.splitext(path_to_file)[-1].lower() + if ext not in IMAGE_EXTENSIONS: + logger.info(( + "File extension \"{}\" is not supported by OpenImageIO." + " Trying to get metadata using FFprobe." + ).format(ext)) + ffprobe_stream = get_ffprobe_data(path_to_file, logger) + if "streams" in ffprobe_stream and len(ffprobe_stream["streams"]) > 0: + metadata_stream = _ffprobe_metadata_conversion( + ffprobe_stream["streams"][0]) + + if not metadata_stream and is_oiio_supported(): + oiio_stream = get_oiio_info_for_input(path_to_file, logger=logger) + if "attribs" in (oiio_stream or {}): + metadata_stream = {} + for key, val in oiio_stream["attribs"].items(): + if "smpte:" in key.lower(): + key = key.replace("smpte:", "") + metadata_stream[key.lower()] = val + for key, val in oiio_stream.items(): + if key == "attribs": + continue + metadata_stream[key] = val + else: + logger.info(( + "OpenImageIO is not supported on this system." + " Trying to get metadata using FFprobe." + )) + ffprobe_stream = get_ffprobe_data(path_to_file, logger) + if "streams" in ffprobe_stream and len(ffprobe_stream["streams"]) > 0: + metadata_stream = _ffprobe_metadata_conversion( + ffprobe_stream["streams"][0]) + + if not metadata_stream: + logger.warning("Failed to get metadata from image file.") + return {} + + if keys is None: + return metadata_stream + + output = {} + for key in keys: + for k, v in metadata_stream.items(): + if key == k: + output[key] = v + break + if isinstance(v, dict) and key in v: + output[key] = v[key] + break + return output + + def get_ffmpeg_format_args(ffprobe_data, source_ffmpeg_cmd=None): """Copy format from input metadata for output. diff --git a/client/ayon_core/plugins/load/export_otio.py b/client/ayon_core/plugins/load/export_otio.py index 03492e57c0..ac236e1038 100644 --- a/client/ayon_core/plugins/load/export_otio.py +++ b/client/ayon_core/plugins/load/export_otio.py @@ -1,4 +1,3 @@ -import os from pathlib import Path from collections import defaultdict @@ -6,6 +5,7 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_api import get_representations from ayon_core.pipeline import load, Anatomy +from ayon_core.lib import get_image_info_metadata from ayon_core import resources, style from ayon_core.pipeline.load import get_representation_path_with_anatomy from ayon_core.tools.utils import show_message_dialog @@ -44,6 +44,7 @@ class ExportOTIOOptionsDialog(QtWidgets.QDialog): # Not all hosts have OpenTimelineIO available. import opentimelineio as OTIO self.OTIO = OTIO + self.log = log super(ExportOTIOOptionsDialog, self).__init__(parent=parent) @@ -247,12 +248,23 @@ class ExportOTIOOptionsDialog(QtWidgets.QDialog): # Get path to representation with correct frame number repre_path = get_representation_path_with_anatomy( representation, anatomy) + + timecode_start_frame = 0 + if file_metadata := get_image_info_metadata( + repre_path, ["timecode"], self.log + ): + # use otio to convert timecode into frame number + timecode_start_frame = self.OTIO.opentime.from_timecode( + file_metadata["timecode"], framerate) + repre_path = Path(repre_path) first_frame = representation["context"].get("frame") if first_frame is None: range = self.OTIO.opentime.TimeRange( - start_time=self.OTIO.opentime.RationalTime(0, framerate), + start_time=self.OTIO.opentime.RationalTime( + timecode_start_frame, framerate + ), duration=self.OTIO.opentime.RationalTime(frames, framerate), ) # Use 'repre_path' as single file @@ -282,9 +294,11 @@ class ExportOTIOOptionsDialog(QtWidgets.QDialog): frame_padding = len(frame_str) range = self.OTIO.opentime.TimeRange( - start_time=self.OTIO.opentime.RationalTime(0, framerate), + start_time=self.OTIO.opentime.RationalTime( + timecode_start_frame.to_frames(), float(framerate) + ), duration=self.OTIO.opentime.RationalTime( - len(repre_files), framerate) + len(repre_files), framerate), ) media_reference = self.OTIO.schema.ImageSequenceReference( diff --git a/client/ayon_core/tests/lib/test_transcoding.py b/client/ayon_core/tests/lib/test_transcoding.py new file mode 100644 index 0000000000..3f46eb960e --- /dev/null +++ b/client/ayon_core/tests/lib/test_transcoding.py @@ -0,0 +1,53 @@ +import pytest +import logging +from pathlib import Path +from ayon_core.lib.transcoding import get_image_info_metadata + +logger = logging.getLogger('test_transcoding') +logger.setLevel(logging.DEBUG) + + +@pytest.mark.parametrize( + "resources_path_factory, metadata, expected, test_id", + [ + ( + Path(__file__).parent.parent + / "resources" + / "lib" + / "transcoding" + / "a01vfxd_sh010_plateP01_v002.1013.exr", + ["timecode"], + {"timecode": "01:00:06:03"}, + "test_01", + ), + ( + Path(__file__).parent.parent + / "resources" + / "lib" + / "transcoding" + / "a01vfxd_sh010_plateP01_v002.1013.exr", + ["timecode", "width", "height", "duration"], + {"timecode": "01:00:06:03", "width": 1920, "height": 1080}, + "test_02", + ), + ( + Path(__file__).parent.parent + / "resources" + / "lib" + / "transcoding" + / "a01vfxd_sh010_plateP01_v002.mov", + ["width", "height", "duration"], + {"width": 1920, "height": 1080, "duration": "0.041708"}, + "test_03", + ), + ], +) +def test_get_image_info_metadata_happy_path( + resources_path_factory, metadata, expected, test_id +): + path_to_file = resources_path_factory.as_posix() + + returned_data = get_image_info_metadata(path_to_file, metadata, logger) + logger.debug(f"Returned data: {returned_data}") + + assert returned_data == expected diff --git a/client/ayon_core/tests/resources/lib/transcoding/a01vfxd_sh010_plateP01_v002.1013.exr b/client/ayon_core/tests/resources/lib/transcoding/a01vfxd_sh010_plateP01_v002.1013.exr new file mode 100644 index 0000000000..9f7bc625bc Binary files /dev/null and b/client/ayon_core/tests/resources/lib/transcoding/a01vfxd_sh010_plateP01_v002.1013.exr differ diff --git a/client/ayon_core/tests/resources/lib/transcoding/a01vfxd_sh010_plateP01_v002.mov b/client/ayon_core/tests/resources/lib/transcoding/a01vfxd_sh010_plateP01_v002.mov new file mode 100644 index 0000000000..7b477114b3 Binary files /dev/null and b/client/ayon_core/tests/resources/lib/transcoding/a01vfxd_sh010_plateP01_v002.mov differ