diff --git a/openpype/hosts/harmony/plugins/publish/extract_render.py b/openpype/hosts/harmony/plugins/publish/extract_render.py index 8374a9427a..827b03443c 100644 --- a/openpype/hosts/harmony/plugins/publish/extract_render.py +++ b/openpype/hosts/harmony/plugins/publish/extract_render.py @@ -91,7 +91,8 @@ class ExtractRender(pyblish.api.InstancePlugin): thumbnail_path = os.path.join(path, "thumbnail.png") ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") args = [ - "{}".format(ffmpeg_path), "-y", + ffmpeg_path, + "-y", "-i", os.path.join(path, list(collections[0])[0]), "-vf", "scale=300:-1", "-vframes", "1", diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index b52078fd5f..1c53c3a2ef 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -60,7 +60,8 @@ class ExtractReview(openpype.api.Extractor): # Generate thumbnail. thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") args = [ - "{}".format(ffmpeg_path), "-y", + ffmpeg_path, + "-y", "-i", output_image_path, "-vf", "scale=300:-1", "-vframes", "1", @@ -78,7 +79,8 @@ class ExtractReview(openpype.api.Extractor): # Generate mov. mov_path = os.path.join(staging_dir, "review.mov") args = [ - ffmpeg_path, "-y", + ffmpeg_path, + "-y", "-i", output_image_path, "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-vframes", "1", diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 0792254716..cdbfe942f0 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -101,11 +101,14 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): jpeg_items.append("\"{}\"".format(full_thumbnail_path)) subprocess_jpeg = " ".join(jpeg_items) + subprocess_args = openpype.lib.split_command_to_list( + subprocess_jpeg + ) # run subprocess - self.log.debug("Executing: {}".format(subprocess_jpeg)) + self.log.debug("Executing: {}".format(" ".join(subprocess_args))) openpype.api.run_subprocess( - subprocess_jpeg, shell=True, logger=self.log + subprocess_args, shell=True, logger=self.log ) # remove thumbnail key from origin repre diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py index 059ac9603c..1cbf186a6c 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py @@ -59,32 +59,35 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): if "trimming" not in fml ] - args = [ - f"\"{ffmpeg_path}\"", + ffmpeg_args = [ + ffmpeg_path, "-ss", str(start / fps), - "-i", f"\"{video_file_path}\"", + "-i", video_file_path, "-t", str(dur / fps) ] if ext in [".mov", ".mp4"]: - args.extend([ + ffmpeg_args.extend([ "-crf", "18", - "-pix_fmt", "yuv420p"]) + "-pix_fmt", "yuv420p" + ]) elif ext in ".wav": - args.extend([ - "-vn -acodec pcm_s16le", - "-ar 48000 -ac 2" + ffmpeg_args.extend([ + "-vn", + "-acodec", "pcm_s16le", + "-ar", "48000", + "-ac", "2" ]) # add output path - args.append(f"\"{clip_trimed_path}\"") + ffmpeg_args.append(clip_trimed_path) - self.log.info(f"Processing: {args}") - ffmpeg_args = " ".join(args) + joined_args = " ".join(ffmpeg_args) + self.log.info(f"Processing: {joined_args}") openpype.api.run_subprocess( ffmpeg_args, shell=True, logger=self.log ) - repr = { + repre = { "name": ext[1:], "ext": ext[1:], "files": os.path.basename(clip_trimed_path), @@ -97,10 +100,10 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): } if ext in [".mov", ".mp4"]: - repr.update({ + repre.update({ "thumbnail": True, "tags": ["review", "ftrackreview", "delete"]}) - instance.data["representations"].append(repr) + instance.data["representations"].append(repre) self.log.debug(f"Instance data: {pformat(instance.data)}") diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 0ead28289b..4cf4a2f8ef 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -27,6 +27,8 @@ from .execute import ( get_pype_execute_args, execute, run_subprocess, + split_command_to_list, + path_to_subprocess_arg, CREATE_NO_WINDOW ) from .log import PypeLogger, timeit @@ -172,6 +174,9 @@ __all__ = [ "get_pype_execute_args", "execute", "run_subprocess", + "split_command_to_list", + "path_to_subprocess_arg", + "CREATE_NO_WINDOW", "env_value_to_bool", "get_paths_from_environ", diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 12fba23e82..3e5b6d3853 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -1,11 +1,10 @@ -import logging import os +import shlex import subprocess +import platform from .log import PypeLogger as Logger -log = logging.getLogger(__name__) - # MSDN process creation flag (Windows only) CREATE_NO_WINDOW = 0x08000000 @@ -100,7 +99,9 @@ def run_subprocess(*args, **kwargs): filtered_env = {str(k): str(v) for k, v in env.items()} # Use lib's logger if was not passed with kwargs. - logger = kwargs.pop("logger", log) + logger = kwargs.pop("logger", None) + if logger is None: + logger = Logger.get_logger("run_subprocess") # set overrides kwargs['stdout'] = kwargs.get('stdout', subprocess.PIPE) @@ -138,6 +139,44 @@ def run_subprocess(*args, **kwargs): return full_output +def path_to_subprocess_arg(path): + """Prepare path for subprocess arguments. + + Returned path can be wrapped with quotes or kept as is. + """ + return subprocess.list2cmdline([path]) + + +def split_command_to_list(string_command): + """Split string subprocess command to list. + + Should be able to split complex subprocess command to separated arguments: + `"C:\\ffmpeg folder\\ffmpeg.exe" -i \"D:\\input.mp4\\" \"D:\\output.mp4\"` + + Should result into list: + `["C:\ffmpeg folder\ffmpeg.exe", "-i", "D:\input.mp4", "D:\output.mp4"]` + + This may be required on few versions of python where subprocess can handle + only list of arguments. + + To be able do that is using `shlex` python module. + + Args: + string_command(str): Full subprocess command. + + Returns: + list: Command separated into individual arguments. + """ + if not string_command: + return [] + + kwargs = {} + # Use 'posix' argument only on windows + if platform.system().lower() == "windows": + kwargs["posix"] = False + return shlex.split(string_command, **kwargs) + + def get_pype_execute_args(*args): """Arguments to run pype command. diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index ae691285b5..31e58025d5 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -1,10 +1,17 @@ import os import pyblish.api -import openpype.api -import openpype.lib -from openpype.lib import should_decompress, \ - get_decompress_dir, decompress +from openpype.lib import ( + get_ffmpeg_tool_path, + + run_subprocess, + split_command_to_list, + path_to_subprocess_arg, + + should_decompress, + get_decompress_dir, + decompress +) import shutil @@ -85,17 +92,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.info("output {}".format(full_output_path)) - ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") + ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} jpeg_items = [] - jpeg_items.append("\"{}\"".format(ffmpeg_path)) + jpeg_items.append(path_to_subprocess_arg(ffmpeg_path)) # override file if already exists jpeg_items.append("-y") # use same input args like with mov jpeg_items.extend(ffmpeg_args.get("input") or []) # input file - jpeg_items.append("-i \"{}\"".format(full_input_path)) + jpeg_items.append("-i {}".format( + path_to_subprocess_arg(full_input_path) + )) # output arguments from presets jpeg_items.extend(ffmpeg_args.get("output") or []) @@ -104,15 +113,16 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): jpeg_items.append("-vframes 1") # output file - jpeg_items.append("\"{}\"".format(full_output_path)) + jpeg_items.append(path_to_subprocess_arg(full_output_path)) - subprocess_jpeg = " ".join(jpeg_items) + subprocess_command = " ".join(jpeg_items) + subprocess_args = split_command_to_list(subprocess_command) # run subprocess - self.log.debug("{}".format(subprocess_jpeg)) + self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform - openpype.api.run_subprocess( - subprocess_jpeg, shell=True, logger=self.log + run_subprocess( + subprocess_args, shell=True, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 2dc822fb0e..2cdc072ffd 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -2,7 +2,9 @@ import os import pyblish import openpype.api from openpype.lib import ( - get_ffmpeg_tool_path + get_ffmpeg_tool_path, + split_command_to_list, + path_to_subprocess_arg ) import tempfile import opentimelineio as otio @@ -56,14 +58,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): audio_inputs.insert(0, empty) # create cmd - cmd = '"{}"'.format(self.ffmpeg_path) + " " + cmd = path_to_subprocess_arg(self.ffmpeg_path) + " " cmd += self.create_cmd(audio_inputs) - cmd += "\"{}\"".format(audio_temp_fpath) + cmd += path_to_subprocess_arg(audio_temp_fpath) + + # Split command to list for subprocess + cmd_list = split_command_to_list(cmd) # run subprocess self.log.debug("Executing: {}".format(cmd)) openpype.api.run_subprocess( - cmd, logger=self.log + cmd_list, logger=self.log ) # remove empty @@ -99,16 +104,16 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # temp audio file audio_fpath = self.create_temp_file(name) - cmd = " ".join([ - '"{}"'.format(self.ffmpeg_path), - "-ss {}".format(start_sec), - "-t {}".format(duration_sec), - "-i \"{}\"".format(audio_file), + cmd = [ + self.ffmpeg_path, + "-ss", str(start_sec), + "-t", str(duration_sec), + "-i", audio_file, audio_fpath - ]) + ] # run subprocess - self.log.debug("Executing: {}".format(cmd)) + self.log.debug("Executing: {}".format(" ".join(cmd))) openpype.api.run_subprocess( cmd, logger=self.log ) @@ -220,17 +225,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): max_duration_sec = max(end_secs) # create empty cmd - cmd = " ".join([ - '"{}"'.format(self.ffmpeg_path), - "-f lavfi", - "-i anullsrc=channel_layout=stereo:sample_rate=48000", - "-t {}".format(max_duration_sec), - "\"{}\"".format(empty_fpath) - ]) + cmd = [ + self.ffmpeg_path, + "-f", "lavfi", + "-i", "anullsrc=channel_layout=stereo:sample_rate=48000", + "-t", str(max_duration_sec), + empty_fpath + ] # generate empty with ffmpeg # run subprocess - self.log.debug("Executing: {}".format(cmd)) + self.log.debug("Executing: {}".format(" ".join(cmd))) openpype.api.run_subprocess( cmd, logger=self.log @@ -261,10 +266,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): for index, input in enumerate(inputs): input_format = input.copy() input_format.update({"i": index}) + input_format["mediaPath"] = path_to_subprocess_arg( + input_format["mediaPath"] + ) + _inputs += ( "-ss {startSec} " "-t {durationSec} " - "-i \"{mediaPath}\" " + "-i {mediaPath} " ).format(**input_format) _filters += "[{i}]adelay={delayMilSec}:all=1[r{i}]; ".format( diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index 818903b54b..ed2ba017d5 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -312,7 +312,7 @@ class ExtractOTIOReview(openpype.api.Extractor): out_frame_start += end_offset # start command list - command = ['"{}"'.format(ffmpeg_path)] + command = [ffmpeg_path] if sequence: input_dir, collection = sequence @@ -324,8 +324,8 @@ class ExtractOTIOReview(openpype.api.Extractor): # form command for rendering gap files command.extend([ - "-start_number {}".format(in_frame_start), - "-i \"{}\"".format(input_path) + "-start_number", str(in_frame_start), + "-i", input_path ]) elif video: @@ -334,13 +334,15 @@ class ExtractOTIOReview(openpype.api.Extractor): input_fps = otio_range.start_time.rate frame_duration = otio_range.duration.value sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) - sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps) + sec_duration = openpype.lib.frames_to_secons( + frame_duration, input_fps + ) # form command for rendering gap files command.extend([ - "-ss {}".format(sec_start), - "-t {}".format(sec_duration), - "-i \"{}\"".format(video_path) + "-ss", str(sec_start), + "-t", str(sec_duration), + "-i", video_path ]) elif gap: @@ -349,22 +351,24 @@ class ExtractOTIOReview(openpype.api.Extractor): # form command for rendering gap files command.extend([ - "-t {} -r {}".format(sec_duration, self.actual_fps), - "-f lavfi", - "-i color=c=black:s={}x{}".format(self.to_width, - self.to_height), - "-tune stillimage" + "-t", str(sec_duration), + "-r", str(self.actual_fps), + "-f", "lavfi", + "-i", "color=c=black:s={}x{}".format( + self.to_width, self.to_height + ), + "-tune", "stillimage" ]) # add output attributes command.extend([ - "-start_number {}".format(out_frame_start), - "\"{}\"".format(output_path) + "-start_number", str(out_frame_start), + output_path ]) # execute self.log.debug("Executing: {}".format(" ".join(command))) output = openpype.api.run_subprocess( - " ".join(command), logger=self.log + command, logger=self.log ) self.log.debug("Output: {}".format(output)) diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index fdb7c4b096..3e2d39c99c 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -75,7 +75,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): output_path = self._get_ffmpeg_output(input_file_path) # start command list - command = ['"{}"'.format(ffmpeg_path)] + command = [ffmpeg_path] video_path = input_file_path frame_start = otio_range.start_time.value @@ -86,17 +86,17 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): # form command for rendering gap files command.extend([ - "-ss {}".format(sec_start), - "-t {}".format(sec_duration), - "-i \"{}\"".format(video_path), - "-c copy", + "-ss", str(sec_start), + "-t", str(sec_duration), + "-i", video_path, + "-c", "copy", output_path ]) # execute self.log.debug("Executing: {}".format(" ".join(command))) output = openpype.api.run_subprocess( - " ".join(command), logger=self.log + command, logger=self.log ) self.log.debug("Output: {}".format(output)) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 78cbea10be..ecc49a8da6 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -13,6 +13,10 @@ import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, ffprobe_streams, + + split_command_to_list, + path_to_subprocess_arg, + should_decompress, get_decompress_dir, decompress @@ -216,12 +220,15 @@ class ExtractReview(pyblish.api.InstancePlugin): raise NotImplementedError subprcs_cmd = " ".join(ffmpeg_args) + subprocess_args = split_command_to_list(subprcs_cmd) # run subprocess - self.log.debug("Executing: {}".format(subprcs_cmd)) + self.log.debug( + "Executing: {}".format(" ".join(subprocess_args)) + ) openpype.api.run_subprocess( - subprcs_cmd, shell=True, logger=self.log + subprocess_args, shell=True, logger=self.log ) # delete files added to fill gaps @@ -480,7 +487,9 @@ class ExtractReview(pyblish.api.InstancePlugin): # Add video/image input path ffmpeg_input_args.append( - "-i \"{}\"".format(temp_data["full_input_path"]) + "-i {}".format( + path_to_subprocess_arg(temp_data["full_input_path"]) + ) ) # Add audio arguments if there are any. Skipped when output are images. @@ -538,7 +547,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # NOTE This must be latest added item to output arguments. ffmpeg_output_args.append( - "\"{}\"".format(temp_data["full_output_path"]) + path_to_subprocess_arg(temp_data["full_output_path"]) ) return self.ffmpeg_full_args( @@ -607,7 +616,7 @@ class ExtractReview(pyblish.api.InstancePlugin): audio_filters.append(arg) all_args = [] - all_args.append("\"{}\"".format(self.ffmpeg_path)) + all_args.append(path_to_subprocess_arg(self.ffmpeg_path)) all_args.extend(input_args) if video_filters: all_args.append("-filter:v") @@ -854,7 +863,9 @@ class ExtractReview(pyblish.api.InstancePlugin): audio_in_args.append("-to {:0.10f}".format(audio_duration)) # Add audio input path - audio_in_args.append("-i \"{}\"".format(audio["filename"])) + audio_in_args.append("-i {}".format( + path_to_subprocess_arg(audio["filename"]) + )) # NOTE: These were changed from input to output arguments. # NOTE: value in "-ac" was hardcoded to 2, changed to audio inputs len. diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 2b07d7db74..4d26fd1ebc 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -117,11 +117,13 @@ class ExtractReviewSlate(openpype.api.Extractor): input_args.extend(repre["_profile"].get('input', [])) else: input_args.extend(repre["outputDef"].get('input', [])) - input_args.append("-loop 1 -i {}".format(slate_path)) + input_args.append("-loop 1 -i {}".format( + openpype.lib.path_to_subprocess_arg(slate_path) + )) input_args.extend([ "-r {}".format(fps), - "-t 0.04"] - ) + "-t 0.04" + ]) if use_legacy_code: codec_args = repre["_profile"].get('codec', []) @@ -188,20 +190,26 @@ class ExtractReviewSlate(openpype.api.Extractor): output_args.append("-y") slate_v_path = slate_path.replace(".png", ext) - output_args.append(slate_v_path) + output_args.append( + openpype.lib.path_to_subprocess_arg(slate_v_path) + ) _remove_at_end.append(slate_v_path) slate_args = [ - "\"{}\"".format(ffmpeg_path), + openpype.lib.path_to_subprocess_arg(ffmpeg_path), " ".join(input_args), " ".join(output_args) ] - slate_subprcs_cmd = " ".join(slate_args) + slate_subprocess_args = openpype.lib.split_command_to_list( + " ".join(slate_args) + ) # run slate generation subprocess - self.log.debug("Slate Executing: {}".format(slate_subprcs_cmd)) + self.log.debug( + "Slate Executing: {}".format(" ".join(slate_subprocess_args)) + ) openpype.api.run_subprocess( - slate_subprcs_cmd, shell=True, logger=self.log + slate_subprocess_args, shell=True, logger=self.log ) # create ffmpeg concat text file path @@ -221,23 +229,22 @@ class ExtractReviewSlate(openpype.api.Extractor): ]) # concat slate and videos together - conc_input_args = ["-y", "-f concat", "-safe 0"] - conc_input_args.append("-i {}".format(conc_text_path)) - - conc_output_args = ["-c copy"] - conc_output_args.append(output_path) - concat_args = [ ffmpeg_path, - " ".join(conc_input_args), - " ".join(conc_output_args) + "-y", + "-f", "concat", + "-safe", "0", + "-i", conc_text_path, + "-c", "copy", + output_path ] - concat_subprcs_cmd = " ".join(concat_args) # ffmpeg concat subprocess - self.log.debug("Executing concat: {}".format(concat_subprcs_cmd)) + self.log.debug( + "Executing concat: {}".format(" ".join(concat_args)) + ) openpype.api.run_subprocess( - concat_subprcs_cmd, shell=True, logger=self.log + concat_args, shell=True, logger=self.log ) self.log.debug("__ repre[tags]: {}".format(repre["tags"]))