diff --git a/openpype/hosts/harmony/plugins/publish/extract_render.py b/openpype/hosts/harmony/plugins/publish/extract_render.py index 38b09902c1..5825d95a4a 100644 --- a/openpype/hosts/harmony/plugins/publish/extract_render.py +++ b/openpype/hosts/harmony/plugins/publish/extract_render.py @@ -94,15 +94,14 @@ class ExtractRender(pyblish.api.InstancePlugin): # Generate thumbnail. thumbnail_path = os.path.join(path, "thumbnail.png") - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") - args = [ - ffmpeg_path, + args = openpype.lib.get_ffmpeg_tool_args( + "ffmpeg", "-y", "-i", os.path.join(path, list(collections[0])[0]), "-vf", "scale=300:-1", "-vframes", "1", thumbnail_path - ] + ) process = subprocess.Popen( args, stdout=subprocess.PIPE, diff --git a/openpype/hosts/hiero/plugins/publish/extract_frames.py b/openpype/hosts/hiero/plugins/publish/extract_frames.py index f865d2fb39..803c338766 100644 --- a/openpype/hosts/hiero/plugins/publish/extract_frames.py +++ b/openpype/hosts/hiero/plugins/publish/extract_frames.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.lib import ( - get_oiio_tools_path, + get_oiio_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -18,7 +18,7 @@ class ExtractFrames(publish.Extractor): movie_extensions = ["mov", "mp4"] def process(self, instance): - oiio_tool_path = get_oiio_tools_path() + oiio_tool_args = get_oiio_tool_args("oiiotool") staging_dir = self.staging_dir(instance) output_template = os.path.join(staging_dir, instance.data["name"]) sequence = instance.context.data["activeTimeline"] @@ -36,7 +36,7 @@ class ExtractFrames(publish.Extractor): output_path = output_template output_path += ".{:04d}.{}".format(int(frame), output_ext) - args = [oiio_tool_path] + args = list(oiio_tool_args) ext = os.path.splitext(input_path)[1][1:] if ext in self.movie_extensions: diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index e2c88ef44a..b13568c781 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -15,8 +15,14 @@ import pyblish.api from maya import cmds # noqa -from openpype.lib.vendor_bin_utils import find_executable -from openpype.lib import source_hash, run_subprocess, get_oiio_tools_path +from openpype.lib import ( + find_executable, + source_hash, + run_subprocess, + get_oiio_tool_args, + ToolNotFoundError, +) + from openpype.pipeline import legacy_io, publish, KnownPublishError from openpype.hosts.maya.api import lib @@ -267,12 +273,11 @@ class MakeTX(TextureProcessor): """ - maketx_path = get_oiio_tools_path("maketx") - - if not maketx_path: - raise AssertionError( - "OIIO 'maketx' tool not found. Result: {}".format(maketx_path) - ) + try: + maketx_args = get_oiio_tool_args("maketx") + except ToolNotFoundError: + raise KnownPublishError( + "OpenImageIO is not available on the machine") # Define .tx filepath in staging if source file is not .tx fname, ext = os.path.splitext(os.path.basename(source)) @@ -328,8 +333,7 @@ class MakeTX(TextureProcessor): self.log.info("Generating .tx file for %s .." % source) - subprocess_args = [ - maketx_path, + subprocess_args = maketx_args + [ "-v", # verbose "-u", # update mode # --checknan doesn't influence the output file but aborts the diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index d5416a389d..4aa7a05bd1 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -1,10 +1,9 @@ import os -import shutil from PIL import Image from openpype.lib import ( run_subprocess, - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, ) from openpype.pipeline import publish from openpype.hosts.photoshop import api as photoshop @@ -85,7 +84,7 @@ class ExtractReview(publish.Extractor): instance.data["representations"].append(repre_skeleton) processed_img_names = [img_list] - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_args = get_ffmpeg_tool_args("ffmpeg") instance.data["stagingDir"] = staging_dir @@ -94,13 +93,21 @@ class ExtractReview(publish.Extractor): source_files_pattern = self._check_and_resize(processed_img_names, source_files_pattern, staging_dir) - self._generate_thumbnail(ffmpeg_path, instance, source_files_pattern, - staging_dir) + self._generate_thumbnail( + list(ffmpeg_args), + instance, + source_files_pattern, + staging_dir) no_of_frames = len(processed_img_names) if no_of_frames > 1: - self._generate_mov(ffmpeg_path, instance, fps, no_of_frames, - source_files_pattern, staging_dir) + self._generate_mov( + list(ffmpeg_args), + instance, + fps, + no_of_frames, + source_files_pattern, + staging_dir) self.log.info(f"Extracted {instance} to {staging_dir}") @@ -142,8 +149,9 @@ class ExtractReview(publish.Extractor): "tags": self.mov_options['tags'] }) - def _generate_thumbnail(self, ffmpeg_path, instance, source_files_pattern, - staging_dir): + def _generate_thumbnail( + self, ffmpeg_args, instance, source_files_pattern, staging_dir + ): """Generates scaled down thumbnail and adds it as representation. Args: @@ -157,8 +165,7 @@ class ExtractReview(publish.Extractor): # Generate thumbnail thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") self.log.info(f"Generate thumbnail {thumbnail_path}") - args = [ - ffmpeg_path, + args = ffmpeg_args + [ "-y", "-i", source_files_pattern, "-vf", "scale=300:-1", diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 9f02d65d00..b99503b3c8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -1,8 +1,9 @@ import os +import subprocess import tempfile import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, get_ffprobe_streams, path_to_subprocess_arg, run_subprocess, @@ -62,12 +63,12 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): instance.context.data["cleanupFullPaths"].append(full_thumbnail_path) - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_executable_args = get_ffmpeg_tool_args("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} jpeg_items = [ - path_to_subprocess_arg(ffmpeg_path), + subprocess.list2cmdline(ffmpeg_executable_args), # override file if already exists "-y" ] diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py index ab5bbc5e2c..c10fc4de97 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py @@ -9,7 +9,8 @@ import json import pyblish.api from openpype.lib import ( - get_oiio_tools_path, + get_oiio_tool_args, + ToolNotFoundError, run_subprocess, ) from openpype.pipeline import KnownPublishError @@ -34,11 +35,12 @@ class ExtractConvertToEXR(pyblish.api.InstancePlugin): if not repres: return - oiio_path = get_oiio_tools_path() - # Raise an exception when oiiotool is not available - # - this can currently happen on MacOS machines - if not os.path.exists(oiio_path): - KnownPublishError( + try: + oiio_args = get_oiio_tool_args("oiiotool") + except ToolNotFoundError: + # Raise an exception when oiiotool is not available + # - this can currently happen on MacOS machines + raise KnownPublishError( "OpenImageIO tool is not available on this machine." ) @@ -64,8 +66,8 @@ class ExtractConvertToEXR(pyblish.api.InstancePlugin): src_filepaths.add(src_filepath) - args = [ - oiio_path, src_filepath, + args = oiio_args + [ + src_filepath, "--compression", self.exr_compression, # TODO how to define color conversion? "--colorconvert", "sRGB", "linear", diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 9065588cf1..40df264452 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -22,11 +22,14 @@ from .events import ( ) from .vendor_bin_utils import ( + ToolNotFoundError, find_executable, get_vendor_bin_path, get_oiio_tools_path, + get_oiio_tool_args, get_ffmpeg_tool_path, - is_oiio_supported + get_ffmpeg_tool_args, + is_oiio_supported, ) from .attribute_definitions import ( @@ -172,7 +175,6 @@ __all__ = [ "emit_event", "register_event_callback", - "find_executable", "get_openpype_execute_args", "get_linux_launcher_args", "execute", @@ -186,9 +188,13 @@ __all__ = [ "env_value_to_bool", "get_paths_from_environ", + "ToolNotFoundError", + "find_executable", "get_vendor_bin_path", "get_oiio_tools_path", + "get_oiio_tool_args", "get_ffmpeg_tool_path", + "get_ffmpeg_tool_args", "is_oiio_supported", "AbstractAttrDef", diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index de6495900e..2bae28786e 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -11,8 +11,8 @@ import xml.etree.ElementTree from .execute import run_subprocess from .vendor_bin_utils import ( - get_ffmpeg_tool_path, - get_oiio_tools_path, + get_ffmpeg_tool_args, + get_oiio_tool_args, is_oiio_supported, ) @@ -83,11 +83,11 @@ def get_oiio_info_for_input(filepath, logger=None, subimages=False): Stdout should contain xml format string. """ - args = [ - get_oiio_tools_path(), + args = get_oiio_tool_args( + "oiiotool", "--info", "-v" - ] + ) if subimages: args.append("-a") @@ -486,12 +486,11 @@ def convert_for_ffmpeg( compression = "none" # Prepare subprocess arguments - oiio_cmd = [ - get_oiio_tools_path(), - + oiio_cmd = get_oiio_tool_args( + "oiiotool", # Don't add any additional attributes "--nosoftwareattrib", - ] + ) # Add input compression if available if compression: oiio_cmd.extend(["--compression", compression]) @@ -656,12 +655,11 @@ def convert_input_paths_for_ffmpeg( for input_path in input_paths: # Prepare subprocess arguments - oiio_cmd = [ - get_oiio_tools_path(), - + oiio_cmd = get_oiio_tool_args( + "oiiotool", # Don't add any additional attributes "--nosoftwareattrib", - ] + ) # Add input compression if available if compression: oiio_cmd.extend(["--compression", compression]) @@ -729,8 +727,8 @@ def get_ffprobe_data(path_to_file, logger=None): logger.info( "Getting information about input \"{}\".".format(path_to_file) ) - args = [ - get_ffmpeg_tool_path("ffprobe"), + ffprobe_args = get_ffmpeg_tool_args("ffprobe") + args = ffprobe_args + [ "-hide_banner", "-loglevel", "fatal", "-show_error", @@ -1084,13 +1082,13 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - oiio_cmd = [ - get_oiio_tools_path(), + oiio_cmd = get_oiio_tool_args( + "oiiotool", input_path, # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path - ] + ) if all([target_colorspace, view, display]): raise ValueError("Colorspace and both screen and display" diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index f27c78d486..dc8bb7435e 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -3,9 +3,15 @@ import logging import platform import subprocess +from openpype import AYON_SERVER_ENABLED + log = logging.getLogger("Vendor utils") +class ToolNotFoundError(Exception): + """Raised when tool arguments are not found.""" + + class CachedToolPaths: """Cache already used and discovered tools and their executables. @@ -252,7 +258,7 @@ def _check_args_returncode(args): return proc.returncode == 0 -def _oiio_executable_validation(filepath): +def _oiio_executable_validation(args): """Validate oiio tool executable if can be executed. Validation has 2 steps. First is using 'find_executable' to fill possible @@ -270,32 +276,63 @@ def _oiio_executable_validation(filepath): should be used. Args: - filepath (str): Path to executable. + args (Union[str, list[str]]): Arguments to launch tool or + path to tool executable. Returns: bool: Filepath is valid executable. """ - filepath = find_executable(filepath) - if not filepath: + if not args: return False - return _check_args_returncode([filepath, "--help"]) + if not isinstance(args, list): + filepath = find_executable(args) + if not filepath: + return False + args = [filepath] + return _check_args_returncode(args + ["--help"]) + + +def _get_ayon_oiio_tool_args(tool_name): + try: + # Use 'ayon-third-party' addon to get oiio arguments + from ayon_third_party import get_oiio_arguments + except Exception: + print("!!! Failed to import 'ayon_third_party' addon.") + return None + + try: + return get_oiio_arguments(tool_name) + except Exception as exc: + print("!!! Failed to get OpenImageIO args. Reason: {}".format(exc)) + return None def get_oiio_tools_path(tool="oiiotool"): - """Path to vendorized OpenImageIO tool executables. + """Path to OpenImageIO tool executables. - On Window it adds .exe extension if missing from tool argument. + On Windows it adds .exe extension if missing from tool argument. Args: - tool (string): Tool name (oiiotool, maketx, ...). + tool (string): Tool name 'oiiotool', 'maketx', etc. Default is "oiiotool". """ if CachedToolPaths.is_tool_cached(tool): return CachedToolPaths.get_executable_path(tool) + if AYON_SERVER_ENABLED: + args = _get_ayon_oiio_tool_args(tool) + if args: + if len(args) > 1: + raise ValueError( + "AYON oiio arguments consist of multiple arguments." + ) + tool_executable_path = args[0] + CachedToolPaths.cache_executable_path(tool, tool_executable_path) + return tool_executable_path + custom_paths_str = os.environ.get("OPENPYPE_OIIO_PATHS") or "" tool_executable_path = find_tool_in_custom_paths( custom_paths_str.split(os.pathsep), @@ -321,7 +358,33 @@ def get_oiio_tools_path(tool="oiiotool"): return tool_executable_path -def _ffmpeg_executable_validation(filepath): +def get_oiio_tool_args(tool_name, *extra_args): + """Arguments to launch OpenImageIO tool. + + Args: + tool_name (str): Tool name 'oiiotool', 'maketx', etc. + *extra_args (str): Extra arguments to add to after tool arguments. + + Returns: + list[str]: List of arguments. + """ + + extra_args = list(extra_args) + + if AYON_SERVER_ENABLED: + args = _get_ayon_oiio_tool_args(tool_name) + if args: + return args + extra_args + + path = get_oiio_tools_path(tool_name) + if path: + return [path] + extra_args + raise ToolNotFoundError( + "OIIO '{}' tool not found.".format(tool_name) + ) + + +def _ffmpeg_executable_validation(args): """Validate ffmpeg tool executable if can be executed. Validation has 2 steps. First is using 'find_executable' to fill possible @@ -338,24 +401,45 @@ def _ffmpeg_executable_validation(filepath): It does not validate if the executable is really a ffmpeg tool. Args: - filepath (str): Path to executable. + args (Union[str, list[str]]): Arguments to launch tool or + path to tool executable. Returns: bool: Filepath is valid executable. """ - filepath = find_executable(filepath) - if not filepath: + if not args: return False - return _check_args_returncode([filepath, "-version"]) + if not isinstance(args, list): + filepath = find_executable(args) + if not filepath: + return False + args = [filepath] + return _check_args_returncode(args + ["--help"]) + + +def _get_ayon_ffmpeg_tool_args(tool_name): + try: + # Use 'ayon-third-party' addon to get ffmpeg arguments + from ayon_third_party import get_ffmpeg_arguments + + except Exception: + print("!!! Failed to import 'ayon_third_party' addon.") + return None + + try: + return get_ffmpeg_arguments(tool_name) + except Exception as exc: + print("!!! Failed to get FFmpeg args. Reason: {}".format(exc)) + return None def get_ffmpeg_tool_path(tool="ffmpeg"): """Path to vendorized FFmpeg executable. Args: - tool (string): Tool name (ffmpeg, ffprobe, ...). + tool (str): Tool name 'ffmpeg', 'ffprobe', etc. Default is "ffmpeg". Returns: @@ -365,6 +449,17 @@ def get_ffmpeg_tool_path(tool="ffmpeg"): if CachedToolPaths.is_tool_cached(tool): return CachedToolPaths.get_executable_path(tool) + if AYON_SERVER_ENABLED: + args = _get_ayon_ffmpeg_tool_args(tool) + if args is not None: + if len(args) > 1: + raise ValueError( + "AYON ffmpeg arguments consist of multiple arguments." + ) + tool_executable_path = args[0] + CachedToolPaths.cache_executable_path(tool, tool_executable_path) + return tool_executable_path + custom_paths_str = os.environ.get("OPENPYPE_FFMPEG_PATHS") or "" tool_executable_path = find_tool_in_custom_paths( custom_paths_str.split(os.pathsep), @@ -390,19 +485,44 @@ def get_ffmpeg_tool_path(tool="ffmpeg"): return tool_executable_path +def get_ffmpeg_tool_args(tool_name, *extra_args): + """Arguments to launch FFmpeg tool. + + Args: + tool_name (str): Tool name 'ffmpeg', 'ffprobe', exc. + *extra_args (str): Extra arguments to add to after tool arguments. + + Returns: + list[str]: List of arguments. + """ + + extra_args = list(extra_args) + + if AYON_SERVER_ENABLED: + args = _get_ayon_ffmpeg_tool_args(tool_name) + if args: + return args + extra_args + + executable_path = get_ffmpeg_tool_path(tool_name) + if executable_path: + return [executable_path] + extra_args + raise ToolNotFoundError( + "FFmpeg '{}' tool not found.".format(tool_name) + ) + + def is_oiio_supported(): """Checks if oiiotool is configured for this platform. Returns: bool: OIIO tool executable is available. """ - loaded_path = oiio_path = get_oiio_tools_path() - if oiio_path: - oiio_path = find_executable(oiio_path) - if not oiio_path: - log.debug("OIIOTool is not configured or not present at {}".format( - loaded_path - )) + try: + args = get_oiio_tool_args("oiiotool") + except ToolNotFoundError: + args = None + if not args: + log.debug("OIIOTool is not configured or not present.") return False - return True + return _oiio_executable_validation(args) diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index e19b7eeb13..4f17731452 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -1,7 +1,7 @@ import os import pyblish from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess ) import tempfile @@ -20,9 +20,6 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): label = "Extract OTIO Audio Tracks" hosts = ["hiero", "resolve", "flame"] - # FFmpeg tools paths - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - def process(self, context): """Convert otio audio track's content to audio representations @@ -91,13 +88,13 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # temp audio file audio_fpath = self.create_temp_file(name) - cmd = [ - self.ffmpeg_path, + cmd = get_ffmpeg_tool_args( + "ffmpeg", "-ss", str(start_sec), "-t", str(duration_sec), "-i", audio_file, audio_fpath - ] + ) # run subprocess self.log.debug("Executing: {}".format(" ".join(cmd))) @@ -210,13 +207,13 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): max_duration_sec = max(end_secs) # create empty cmd - cmd = [ - self.ffmpeg_path, + cmd = get_ffmpeg_tool_args( + "ffmpeg", "-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=48000", "-t", str(max_duration_sec), empty_fpath - ] + ) # generate empty with ffmpeg # run subprocess @@ -295,7 +292,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): filters_tmp_filepath = tmp_file.name tmp_file.write(",".join(filters)) - args = [self.ffmpeg_path] + args = get_ffmpeg_tool_args("ffmpeg") args.extend(input_args) args.extend([ "-filter_complex_script", filters_tmp_filepath, diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index 9ebcad2af1..699207df8a 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -20,7 +20,7 @@ import opentimelineio as otio from pyblish import api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -338,8 +338,6 @@ class ExtractOTIOReview(publish.Extractor): Returns: otio.time.TimeRange: trimmed available range """ - # get rendering app path - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") # create path and frame start to destination output_path, out_frame_start = self._get_ffmpeg_output() @@ -348,7 +346,7 @@ class ExtractOTIOReview(publish.Extractor): out_frame_start += end_offset # start command list - command = [ffmpeg_path] + command = get_ffmpeg_tool_args("ffmpeg") input_extension = None if sequence: diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 70726338aa..67ff6c538c 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -11,7 +11,7 @@ from copy import deepcopy import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -75,14 +75,12 @@ class ExtractOTIOTrimmingVideo(publish.Extractor): otio_range (opentime.TimeRange): range to trim to """ - # get rendering app path - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") # create path to destination output_path = self._get_ffmpeg_output(input_file_path) # start command list - command = [ffmpeg_path] + command = get_ffmpeg_tool_args("ffmpeg") video_path = input_file_path frame_start = otio_range.start_time.value diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index f053d1b500..9cc456872e 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -3,6 +3,7 @@ import re import copy import json import shutil +import subprocess from abc import ABCMeta, abstractmethod import six @@ -11,7 +12,7 @@ import speedcopy import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, filter_profiles, path_to_subprocess_arg, run_subprocess, @@ -72,9 +73,6 @@ class ExtractReview(pyblish.api.InstancePlugin): alpha_exts = ["exr", "png", "dpx"] - # FFmpeg tools paths - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - # Preset attributes profiles = None @@ -787,8 +785,9 @@ class ExtractReview(pyblish.api.InstancePlugin): arg = arg.replace(identifier, "").strip() audio_filters.append(arg) - all_args = [] - all_args.append(path_to_subprocess_arg(self.ffmpeg_path)) + all_args = [ + subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")) + ] all_args.extend(input_args) if video_filters: all_args.append("-filter:v") diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index fca3d96ca6..8f31f10c42 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -1,5 +1,6 @@ import os import re +import subprocess from pprint import pformat import pyblish.api @@ -7,7 +8,7 @@ import pyblish.api from openpype.lib import ( path_to_subprocess_arg, run_subprocess, - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, get_ffprobe_data, get_ffprobe_streams, get_ffmpeg_codec_args, @@ -47,8 +48,6 @@ class ExtractReviewSlate(publish.Extractor): self.log.info("_ slates_data: {}".format(pformat(slates_data))) - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - if "reviewToWidth" in inst_data: use_legacy_code = True else: @@ -260,7 +259,7 @@ class ExtractReviewSlate(publish.Extractor): _remove_at_end.append(slate_v_path) slate_args = [ - path_to_subprocess_arg(ffmpeg_path), + subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg")), " ".join(input_args), " ".join(output_args) ] @@ -281,7 +280,6 @@ class ExtractReviewSlate(publish.Extractor): os.path.splitext(slate_v_path)) _remove_at_end.append(slate_silent_path) self._create_silent_slate( - ffmpeg_path, slate_v_path, slate_silent_path, audio_codec, @@ -309,12 +307,12 @@ class ExtractReviewSlate(publish.Extractor): "[0:v] [1:v] concat=n=2:v=1:a=0 [v]", "-map", '[v]' ] - concat_args = [ - ffmpeg_path, + concat_args = get_ffmpeg_tool_args( + "ffmpeg", "-y", "-i", slate_v_path, "-i", input_path, - ] + ) concat_args.extend(fmap) if offset_timecode: concat_args.extend(["-timecode", offset_timecode]) @@ -490,7 +488,6 @@ class ExtractReviewSlate(publish.Extractor): def _create_silent_slate( self, - ffmpeg_path, src_path, dst_path, audio_codec, @@ -515,8 +512,8 @@ class ExtractReviewSlate(publish.Extractor): one_frame_duration = str(int(one_frame_duration)) + "us" self.log.debug("One frame duration is {}".format(one_frame_duration)) - slate_silent_args = [ - ffmpeg_path, + slate_silent_args = get_ffmpeg_tool_args( + "ffmpeg", "-i", src_path, "-f", "lavfi", "-i", "anullsrc=r={}:cl={}:d={}".format( @@ -531,7 +528,7 @@ class ExtractReviewSlate(publish.Extractor): "-shortest", "-y", dst_path - ] + ) # run slate generation subprocess self.log.debug("Silent Slate Executing: {}".format( " ".join(slate_silent_args) diff --git a/openpype/plugins/publish/extract_scanline_exr.py b/openpype/plugins/publish/extract_scanline_exr.py index 0e4c0ca65f..9f22794a79 100644 --- a/openpype/plugins/publish/extract_scanline_exr.py +++ b/openpype/plugins/publish/extract_scanline_exr.py @@ -5,7 +5,12 @@ import shutil import pyblish.api -from openpype.lib import run_subprocess, get_oiio_tools_path +from openpype.lib import ( + run_subprocess, + get_oiio_tool_args, + ToolNotFoundError, +) +from openpype.pipeline import KnownPublishError class ExtractScanlineExr(pyblish.api.InstancePlugin): @@ -45,11 +50,11 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): stagingdir = os.path.normpath(repre.get("stagingDir")) - oiio_tool_path = get_oiio_tools_path() - if not os.path.exists(oiio_tool_path): - self.log.error( - "OIIO tool not found in {}".format(oiio_tool_path)) - raise AssertionError("OIIO tool not found") + try: + oiio_tool_args = get_oiio_tool_args("oiiotool") + except ToolNotFoundError: + self.log.error("OIIO tool not found.") + raise KnownPublishError("OIIO tool not found") for file in input_files: @@ -57,8 +62,7 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): temp_name = os.path.join(stagingdir, "__{}".format(file)) # move original render to temp location shutil.move(original_name, temp_name) - oiio_cmd = [ - oiio_tool_path, + oiio_cmd = oiio_tool_args + [ os.path.join(stagingdir, temp_name), "--scanline", "-o", os.path.join(stagingdir, original_name) ] diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index b98ab64f56..b72a6d02ad 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -1,10 +1,11 @@ import os +import subprocess import tempfile import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, - get_oiio_tools_path, + get_ffmpeg_tool_args, + get_oiio_tool_args, is_oiio_supported, run_subprocess, @@ -174,12 +175,11 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def create_thumbnail_oiio(self, src_path, dst_path): self.log.info("Extracting thumbnail {}".format(dst_path)) - oiio_tool_path = get_oiio_tools_path() - oiio_cmd = [ - oiio_tool_path, + oiio_cmd = get_oiio_tool_args( + "oiiotool", "-a", src_path, "-o", dst_path - ] + ) self.log.debug("running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) @@ -194,27 +194,27 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def create_thumbnail_ffmpeg(self, src_path, dst_path): self.log.info("outputting {}".format(dst_path)) - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_path_args = get_ffmpeg_tool_args("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} - jpeg_items = [] - jpeg_items.append(path_to_subprocess_arg(ffmpeg_path)) - # override file if already exists - jpeg_items.append("-y") + jpeg_items = [ + subprocess.list2cmdline(ffmpeg_path_args) + ] # flag for large file sizes max_int = 2147483647 - jpeg_items.append("-analyzeduration {}".format(max_int)) - jpeg_items.append("-probesize {}".format(max_int)) + jpeg_items.extend([ + "-y", + "-analyzeduration", str(max_int), + "-probesize", str(max_int), + ]) # use same input args like with mov jpeg_items.extend(ffmpeg_args.get("input") or []) # input file - jpeg_items.append("-i {}".format( - path_to_subprocess_arg(src_path) - )) + jpeg_items.extend(["-i", path_to_subprocess_arg(src_path)]) # output arguments from presets jpeg_items.extend(ffmpeg_args.get("output") or []) # we just want one frame from movie files - jpeg_items.append("-vframes 1") + jpeg_items.extend(["-vframes", "1"]) # output file jpeg_items.append(path_to_subprocess_arg(dst_path)) subprocess_command = " ".join(jpeg_items) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index a9c95d6065..54622bb84e 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -17,8 +17,8 @@ import tempfile import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, - get_oiio_tools_path, + get_ffmpeg_tool_args, + get_oiio_tool_args, is_oiio_supported, run_subprocess, @@ -144,12 +144,11 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): def create_thumbnail_oiio(self, src_path, dst_path): self.log.info("outputting {}".format(dst_path)) - oiio_tool_path = get_oiio_tools_path() - oiio_cmd = [ - oiio_tool_path, + oiio_cmd = get_oiio_tool_args( + "oiiotool", "-a", src_path, "-o", dst_path - ] + ) self.log.info("Running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) @@ -162,18 +161,16 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return False def create_thumbnail_ffmpeg(self, src_path, dst_path): - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - max_int = str(2147483647) - ffmpeg_cmd = [ - ffmpeg_path, + ffmpeg_cmd = get_ffmpeg_tool_args( + "ffmpeg", "-y", "-analyzeduration", max_int, "-probesize", max_int, "-i", src_path, "-vframes", "1", dst_path - ] + ) self.log.info("Running: {}".format(" ".join(ffmpeg_cmd))) try: diff --git a/openpype/plugins/publish/extract_trim_video_audio.py b/openpype/plugins/publish/extract_trim_video_audio.py index b951136391..2907ae1839 100644 --- a/openpype/plugins/publish/extract_trim_video_audio.py +++ b/openpype/plugins/publish/extract_trim_video_audio.py @@ -4,7 +4,7 @@ from pprint import pformat import pyblish.api from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, run_subprocess, ) from openpype.pipeline import publish @@ -32,7 +32,7 @@ class ExtractTrimVideoAudio(publish.Extractor): instance.data["representations"] = list() # get ffmpet path - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") + ffmpeg_tool_args = get_ffmpeg_tool_args("ffmpeg") # get staging dir staging_dir = self.staging_dir(instance) @@ -76,8 +76,7 @@ class ExtractTrimVideoAudio(publish.Extractor): if "trimming" not in fml ] - ffmpeg_args = [ - ffmpeg_path, + ffmpeg_args = ffmpeg_tool_args + [ "-ss", str(clip_start_h / fps), "-i", video_file_path, "-t", str(clip_dur_h / fps) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 085b62501c..189feaee3a 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -8,21 +8,15 @@ from string import Formatter import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins from openpype.lib import ( - get_ffmpeg_tool_path, + get_ffmpeg_tool_args, get_ffmpeg_codec_args, get_ffmpeg_format_args, convert_ffprobe_fps_value, - convert_ffprobe_fps_to_float, ) - -ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") -ffprobe_path = get_ffmpeg_tool_path("ffprobe") - - FFMPEG = ( - '"{}"%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' -).format(ffmpeg_path) + '{}%(input_args)s -i "%(input)s" %(filters)s %(args)s%(output)s' +).format(subprocess.list2cmdline(get_ffmpeg_tool_args("ffmpeg"))) DRAWTEXT = ( "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':" @@ -46,14 +40,14 @@ def _get_ffprobe_data(source): :param str source: source media file :rtype: [{}, ...] """ - command = [ - ffprobe_path, + command = get_ffmpeg_tool_args( + "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", source - ] + ) kwargs = { "stdout": subprocess.PIPE, } diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index b17ca0adc8..80d156185b 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -7,8 +7,8 @@ from openpype.style import get_objected_colors from openpype.lib import ( run_subprocess, is_oiio_supported, - get_oiio_tools_path, - get_ffmpeg_tool_path, + get_oiio_tool_args, + get_ffmpeg_tool_args, ) from openpype.lib.transcoding import ( IMAGE_EXTENSIONS, @@ -481,12 +481,12 @@ def _convert_thumbnail_oiio(src_path, dst_path): if not is_oiio_supported(): return None - oiio_cmd = [ - get_oiio_tools_path(), + oiio_cmd = get_oiio_tool_args( + "oiiotool", "-i", src_path, "--subimage", "0", "-o", dst_path - ] + ) try: _run_silent_subprocess(oiio_cmd) except Exception: @@ -495,12 +495,12 @@ def _convert_thumbnail_oiio(src_path, dst_path): def _convert_thumbnail_ffmpeg(src_path, dst_path): - ffmpeg_cmd = [ - get_ffmpeg_tool_path(), + ffmpeg_cmd = get_ffmpeg_tool_args( + "ffmpeg", "-y", "-i", src_path, dst_path - ] + ) try: _run_silent_subprocess(ffmpeg_cmd) except Exception: diff --git a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py index f46e31786c..306c43e85d 100644 --- a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -5,6 +5,8 @@ import clique import subprocess import openpype.lib from qtpy import QtWidgets, QtCore + +from openpype.lib import get_ffprobe_data from . import DropEmpty, ComponentsList, ComponentItem @@ -269,26 +271,8 @@ class DropDataFrame(QtWidgets.QFrame): self._process_data(data) def load_data_with_probe(self, filepath): - ffprobe_path = openpype.lib.get_ffmpeg_tool_path("ffprobe") - args = [ - "\"{}\"".format(ffprobe_path), - '-v', 'quiet', - '-print_format json', - '-show_format', - '-show_streams', - '"{}"'.format(filepath) - ] - ffprobe_p = subprocess.Popen( - ' '.join(args), - stdout=subprocess.PIPE, - shell=True - ) - ffprobe_output = ffprobe_p.communicate()[0] - if ffprobe_p.returncode != 0: - raise RuntimeError( - 'Failed on ffprobe: check if ffprobe path is set in PATH env' - ) - return json.loads(ffprobe_output)['streams'][0] + ffprobe_data = get_ffprobe_data(filepath) + return ffprobe_data["streams"][0] def get_file_data(self, data): filepath = data['files'][0]