mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
process is splitted more than already was
This commit is contained in:
parent
0586fff9ab
commit
9ed201c0cd
1 changed files with 338 additions and 137 deletions
|
|
@ -65,13 +65,17 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
return
|
||||
|
||||
instance_families = self.families_from_instance(instance)
|
||||
profile_outputs = self.filter_outputs_by_families(
|
||||
_profile_outputs = self.filter_outputs_by_families(
|
||||
profile, instance_families
|
||||
)
|
||||
if not profile_outputs:
|
||||
if not _profile_outputs:
|
||||
return
|
||||
|
||||
instance_data = None
|
||||
# Store `filename_suffix` to save to save arguments
|
||||
profile_outputs = []
|
||||
for filename_suffix, definition in _profile_outputs.items():
|
||||
definition["filename_suffix"] = filename_suffix
|
||||
profile_outputs.append(definition)
|
||||
|
||||
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
|
||||
|
|
@ -97,24 +101,14 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
if not outputs:
|
||||
continue
|
||||
|
||||
staging_dir = repre["stagingDir"]
|
||||
|
||||
# Prepare instance data.
|
||||
# NOTE Till this point it is not required to have set most
|
||||
# of keys in instance data. So publishing won't crash if plugin
|
||||
# won't get here and instance miss required keys.
|
||||
if instance_data is None:
|
||||
instance_data = self.prepare_instance_data(instance)
|
||||
|
||||
for filename_suffix, output_def in outputs.items():
|
||||
|
||||
for output_def in outputs:
|
||||
# Create copy of representation
|
||||
new_repre = copy.deepcopy(repre)
|
||||
|
||||
output_ext = output_def.get("ext") or "mov"
|
||||
if output_ext.startswith("."):
|
||||
output_ext = output_ext[1:]
|
||||
|
||||
additional_tags = output_def.get("tags") or []
|
||||
# TODO new method?
|
||||
# `self.prepare_new_repre_tags(new_repre, additional_tags)`
|
||||
|
|
@ -128,152 +122,359 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
new_repre["tags"].append(tag)
|
||||
|
||||
self.log.debug(
|
||||
"New representation ext: \"{}\" | tags: `{}`".format(
|
||||
output_ext, new_repre["tags"]
|
||||
)
|
||||
"New representation tags: `{}`".format(new_repre["tags"])
|
||||
)
|
||||
|
||||
# Output is image file sequence witht frames
|
||||
# TODO change variable to `output_is_sequence`
|
||||
# QUESTION Shall we do it in opposite? Expect that if output
|
||||
# extension is image format and input is sequence or video
|
||||
# format then do sequence and single frame only if tag is
|
||||
# "single-frame" (or similar)
|
||||
# QUESTION Should we check for "sequence" only in additional
|
||||
# tags or in all tags of new representation
|
||||
is_sequence = (
|
||||
"sequence" in additional_tags
|
||||
and (output_ext in self.image_exts)
|
||||
)
|
||||
|
||||
# no handles switch from profile tags
|
||||
no_handles = "no-handles" in additional_tags
|
||||
|
||||
# TODO GLOBAL ISSUE - Find better way how to find out if input
|
||||
# is sequence. Issues( in theory):
|
||||
# - there may be multiple files ant not be sequence
|
||||
# - remainders are not checked at all
|
||||
# - there can be more than one collection
|
||||
if isinstance(repre["files"], (tuple, list)):
|
||||
collections, remainder = clique.assemble(repre["files"])
|
||||
|
||||
full_input_path = os.path.join(
|
||||
staging_dir,
|
||||
collections[0].format("{head}{padding}{tail}")
|
||||
)
|
||||
|
||||
filename = collections[0].format("{head}")
|
||||
if filename.endswith("."):
|
||||
filename = filename[:-1]
|
||||
else:
|
||||
full_input_path = os.path.join(
|
||||
staging_dir, repre["files"]
|
||||
)
|
||||
filename = os.path.splitext(repre["files"])[0]
|
||||
|
||||
# QUESTION This breaks Anatomy template system is it ok?
|
||||
# QUESTION How do we care about multiple outputs with same
|
||||
# extension? (Expect we don't...)
|
||||
# - possible solution add "<{review_suffix}>" into templates
|
||||
# but that may cause issues when clients remove that.
|
||||
if is_sequence:
|
||||
filename_base = filename + "_{0}".format(filename_suffix)
|
||||
repr_file = filename_base + ".%08d.{0}".format(
|
||||
output_ext
|
||||
)
|
||||
new_repre["sequence_file"] = repr_file
|
||||
full_output_path = os.path.join(
|
||||
staging_dir, filename_base, repr_file
|
||||
)
|
||||
|
||||
else:
|
||||
repr_file = filename + "_{0}.{1}".format(
|
||||
filename_suffix, output_ext
|
||||
)
|
||||
full_output_path = os.path.join(staging_dir, repr_file)
|
||||
|
||||
self.log.info("Input path {}".format(full_input_path))
|
||||
self.log.info("Output path {}".format(full_output_path))
|
||||
|
||||
# QUESTION Why the hell we do this?
|
||||
# QUESTION Why the hell we do this, adding tags to families?
|
||||
# add families
|
||||
for tag in additional_tags:
|
||||
if tag not in instance.data["families"]:
|
||||
instance.data["families"].append(tag)
|
||||
|
||||
ffmpeg_args = self._ffmpeg_arguments(
|
||||
output_def, instance, instance_data
|
||||
)
|
||||
ffmpeg_args = self._ffmpeg_arguments(output_def, instance)
|
||||
|
||||
def _ffmpeg_arguments(output_def, instance, repre, instance_data):
|
||||
# TODO split into smaller methods and use these variable only there
|
||||
fps = instance_data["fps"]
|
||||
frame_start = instance_data["frame_start"]
|
||||
frame_end = instance_data["frame_end"]
|
||||
handle_start = instance_data["handle_start"]
|
||||
handle_end = instance_data["handle_end"]
|
||||
frame_start_handle = frame_start - handle_start,
|
||||
frame_end_handle = frame_end + handle_end,
|
||||
pixel_aspect = instance_data["pixel_aspect"]
|
||||
resolution_width = instance_data["resolution_width"]
|
||||
resolution_height = instance_data["resolution_height"]
|
||||
def repre_has_sequence(self, repre):
|
||||
# TODO GLOBAL ISSUE - Find better way how to find out if input
|
||||
# is sequence. Issues( in theory):
|
||||
# - there may be multiple files ant not be sequence
|
||||
# - remainders are not checked at all
|
||||
# - there can be more than one collection
|
||||
return isinstance(repre["files"], (list, tuple))
|
||||
|
||||
def _ffmpeg_arguments(self, output_def, instance, repre):
|
||||
temp_data = self.prepare_temp_data(instance)
|
||||
|
||||
# NOTE used different key for final frame start/end to not confuse
|
||||
# those who don't know what
|
||||
# - e.g. "frame_start_output"
|
||||
# QUESTION should we use tags ONLY from output definition?
|
||||
# - In that case `output_def.get("tags") or []` should replace
|
||||
# `repre["tags"]`.
|
||||
# Change output frames when output should be without handles
|
||||
no_handles = "no-handles" in repre["tags"]
|
||||
if no_handles:
|
||||
temp_data["output_frame_start"] = temp_data["frame_start"]
|
||||
temp_data["output_frame_end"] = temp_data["frame_end"]
|
||||
|
||||
# TODO this may hold class which may be easier to work with
|
||||
# Get FFmpeg arguments from profile presets
|
||||
output_ffmpeg_args = output_def.get("ffmpeg_args") or {}
|
||||
output_ffmpeg_input = output_ffmpeg_args.get("input") or []
|
||||
output_ffmpeg_filters = output_ffmpeg_args.get("filters") or []
|
||||
output_ffmpeg_output = output_ffmpeg_args.get("output") or []
|
||||
out_def_ffmpeg_args = output_def.get("ffmpeg_args") or {}
|
||||
|
||||
ffmpeg_input_args = []
|
||||
ffmpeg_output_args = []
|
||||
ffmpeg_input_args = out_def_ffmpeg_args.get("input") or []
|
||||
ffmpeg_output_args = out_def_ffmpeg_args.get("output") or []
|
||||
ffmpeg_video_filters = out_def_ffmpeg_args.get("video_filters") or []
|
||||
ffmpeg_audio_filters = out_def_ffmpeg_args.get("audio_filters") or []
|
||||
|
||||
# Override output file
|
||||
# Add argument to override output file
|
||||
ffmpeg_input_args.append("-y")
|
||||
# Add input args from presets
|
||||
ffmpeg_input_args.extend(output_ffmpeg_input)
|
||||
|
||||
if isinstance(repre["files"], list):
|
||||
# QUESTION What is sence of this?
|
||||
if frame_start_handle != repre.get(
|
||||
"detectedStart", frame_start_handle
|
||||
):
|
||||
frame_start_handle = repre.get("detectedStart")
|
||||
if no_handles:
|
||||
# NOTE used `-frames:v` instead of `-t`
|
||||
duration_frames = (
|
||||
temp_data["output_frame_end"]
|
||||
- temp_data["output_frame_start"]
|
||||
+ 1
|
||||
)
|
||||
ffmpeg_output_args.append("-frames:v {}".format(duration_frames))
|
||||
|
||||
# exclude handle if no handles defined
|
||||
if no_handles:
|
||||
frame_start_handle = frame_start
|
||||
frame_end_handle = frame_end
|
||||
if self.repre_has_sequence(repre):
|
||||
# NOTE removed "detectedStart" key handling (NOT SET)
|
||||
|
||||
# Set start frame
|
||||
ffmpeg_input_args.append(
|
||||
"-start_number {0} -framerate {1}".format(
|
||||
frame_start_handle, fps))
|
||||
else:
|
||||
if no_handles:
|
||||
# QUESTION why we are using seconds instead of frames?
|
||||
start_sec = float(handle_start) / fps
|
||||
ffmpeg_input_args.append("-ss {:0.2f}".format(start_sec))
|
||||
frame_start_handle = frame_start
|
||||
frame_end_handle = frame_end
|
||||
"-start_number {}".format(temp_data["output_frame_start"])
|
||||
)
|
||||
|
||||
# TODO add fps mapping `{fps: fraction}`
|
||||
# - e.g.: {
|
||||
# "25": "25/1",
|
||||
# "24": "24/1",
|
||||
# "23.976": "24000/1001"
|
||||
# }
|
||||
# Add framerate to input when input is sequence
|
||||
ffmpeg_input_args.append(
|
||||
"-framerate {}".format(temp_data["fps"])
|
||||
)
|
||||
|
||||
elif no_handles:
|
||||
# QUESTION Shall we change this to use filter:
|
||||
# `select="gte(n\,handle_start),setpts=PTS-STARTPTS`
|
||||
# Pros:
|
||||
# 1.) Python is not good at float operation
|
||||
# 2.) FPS on instance may not be same as input's
|
||||
start_sec = float(temp_data["handle_start"]) / temp_data["fps"]
|
||||
ffmpeg_input_args.append("-ss {:0.2f}".format(start_sec))
|
||||
|
||||
full_input_path, full_output_path = self.input_output_paths(
|
||||
repre, output_def
|
||||
)
|
||||
ffmpeg_input_args.append("-i \"{}\"".format(full_input_path))
|
||||
|
||||
# Add audio arguments if there are any
|
||||
audio_in_args, audio_filters, audio_out_args = self.audio_args(
|
||||
instance, temp_data
|
||||
)
|
||||
ffmpeg_input_args.extend(audio_in_args)
|
||||
ffmpeg_audio_filters.extend(audio_filters)
|
||||
ffmpeg_output_args.extend(audio_out_args)
|
||||
|
||||
# In case audio is longer than video.
|
||||
# QUESTION what if audio is shoter than video?
|
||||
if "-shortest" not in ffmpeg_output_args:
|
||||
ffmpeg_output_args.append("-shortest")
|
||||
|
||||
ffmpeg_output_args.append("\"{}\"".format(full_output_path))
|
||||
|
||||
def prepare_temp_data(self, instance):
|
||||
frame_start = instance.data["frameStart"]
|
||||
handle_start = instance.data.get(
|
||||
"handleStart",
|
||||
instance.context.data["handleStart"]
|
||||
)
|
||||
frame_end = instance.data["frameEnd"]
|
||||
handle_end = instance.data.get(
|
||||
"handleEnd",
|
||||
instance.context.data["handleEnd"]
|
||||
)
|
||||
|
||||
frame_start_handle = frame_start - handle_start
|
||||
frame_end_handle = frame_end + handle_end
|
||||
|
||||
def prepare_instance_data(self, instance):
|
||||
return {
|
||||
"fps": float(instance.data["fps"]),
|
||||
"frame_start": instance.data["frameStart"],
|
||||
"frame_end": instance.data["frameEnd"],
|
||||
"handle_start": instance.data.get(
|
||||
"handleStart",
|
||||
instance.context.data["handleStart"]
|
||||
),
|
||||
"handle_end": instance.data.get(
|
||||
"handleEnd",
|
||||
instance.context.data["handleEnd"]
|
||||
),
|
||||
"frame_start": frame_start,
|
||||
"frame_end": frame_end,
|
||||
"handle_start": handle_start,
|
||||
"handle_end": handle_end,
|
||||
"frame_start_handle": frame_start_handle,
|
||||
"frame_end_handle": frame_end_handle,
|
||||
"output_frame_start": frame_start_handle,
|
||||
"output_frame_end": frame_end_handle,
|
||||
"pixel_aspect": instance.data.get("pixelAspect", 1),
|
||||
"resolution_width": instance.data.get("resolutionWidth"),
|
||||
"resolution_height": instance.data.get("resolutionHeight"),
|
||||
}
|
||||
|
||||
def input_output_paths(self, repre, output_def):
|
||||
staging_dir = repre["stagingDir"]
|
||||
|
||||
# TODO Define if extension should have dot or not
|
||||
output_ext = output_def.get("ext") or "mov"
|
||||
if output_ext.startswith("."):
|
||||
output_ext = output_ext[1:]
|
||||
|
||||
self.log.debug(
|
||||
"New representation ext: `{}`".format(output_ext)
|
||||
)
|
||||
|
||||
# Output is image file sequence witht frames
|
||||
# QUESTION Shall we do it in opposite? Expect that if output
|
||||
# extension is image format and input is sequence or video
|
||||
# format then do sequence and single frame only if tag is
|
||||
# "single-frame" (or similar)
|
||||
# QUESTION should we use tags ONLY from output definition?
|
||||
# - In that case `output_def.get("tags") or []` should replace
|
||||
# `repre["tags"]`.
|
||||
output_is_sequence = (
|
||||
"sequence" in repre["tags"]
|
||||
and (output_ext in self.image_exts)
|
||||
)
|
||||
|
||||
if self.repre_has_sequence(repre):
|
||||
collections, remainder = clique.assemble(repre["files"])
|
||||
|
||||
full_input_path = os.path.join(
|
||||
staging_dir,
|
||||
collections[0].format("{head}{padding}{tail}")
|
||||
)
|
||||
|
||||
filename = collections[0].format("{head}")
|
||||
if filename.endswith("."):
|
||||
filename = filename[:-1]
|
||||
else:
|
||||
full_input_path = os.path.join(
|
||||
staging_dir, repre["files"]
|
||||
)
|
||||
filename = os.path.splitext(repre["files"])[0]
|
||||
|
||||
filename_suffix = output_def["filename_suffix"]
|
||||
# QUESTION This breaks Anatomy template system is it ok?
|
||||
# QUESTION How do we care about multiple outputs with same
|
||||
# extension? (Expect we don't...)
|
||||
# - possible solution add "<{review_suffix}>" into templates
|
||||
# but that may cause issues when clients remove that (and it's
|
||||
# ugly).
|
||||
if output_is_sequence:
|
||||
filename_base = "{}_{}".format(
|
||||
filename, filename_suffix
|
||||
)
|
||||
repr_file = "{}.%08d.{}".format(
|
||||
filename_base, output_ext
|
||||
)
|
||||
|
||||
repre["sequence_file"] = repr_file
|
||||
full_output_path = os.path.join(
|
||||
staging_dir, filename_base, repr_file
|
||||
)
|
||||
|
||||
else:
|
||||
repr_file = "{}_{}.{}".format(
|
||||
filename, filename_suffix, output_ext
|
||||
)
|
||||
full_output_path = os.path.join(staging_dir, repr_file)
|
||||
|
||||
self.log.debug("Input path {}".format(full_input_path))
|
||||
self.log.debug("Output path {}".format(full_output_path))
|
||||
|
||||
return full_input_path, full_output_path
|
||||
|
||||
def audio_args(self, instance, temp_data):
|
||||
audio_in_args = []
|
||||
audio_filters = []
|
||||
audio_out_args = []
|
||||
audio_inputs = instance.data.get("audio")
|
||||
if not audio_inputs:
|
||||
return audio_in_args, audio_filters, audio_out_args
|
||||
|
||||
for audio in audio_inputs:
|
||||
# NOTE modified, always was expected "frameStartFtrack" which is
|
||||
# STANGE?!!!
|
||||
# TODO use different frame start!
|
||||
offset_seconds = 0
|
||||
frame_start_ftrack = instance.data.get("frameStartFtrack")
|
||||
if frame_start_ftrack is not None:
|
||||
offset_frames = frame_start_ftrack - audio["offset"]
|
||||
offset_seconds = offset_frames / temp_data["fps"]
|
||||
|
||||
if offset_seconds > 0:
|
||||
audio_in_args.append(
|
||||
"-ss {}".format(offset_seconds)
|
||||
)
|
||||
elif offset_seconds < 0:
|
||||
audio_in_args.append(
|
||||
"-itsoffset {}".format(abs(offset_seconds))
|
||||
)
|
||||
|
||||
audio_in_args.append("-i \"{}\"".format(audio["filename"]))
|
||||
|
||||
# NOTE: These were changed from input to output arguments.
|
||||
# NOTE: value in "-ac" was hardcoded to 2, changed to audio inputs len.
|
||||
# Need to merge audio if there are more than 1 input.
|
||||
if len(audio_inputs) > 1:
|
||||
audio_out_args.append("-filter_complex amerge")
|
||||
audio_out_args.append("-ac {}".format(len(audio_inputs)))
|
||||
|
||||
return audio_in_args, audio_filters, audio_out_args
|
||||
|
||||
def resolution_ratios(self, temp_data, output_def, repre):
|
||||
output_width = output_def.get("width")
|
||||
output_height = output_def.get("height")
|
||||
output_pixel_aspect = output_def.get("aspect_ratio")
|
||||
output_letterbox = output_def.get("letter_box")
|
||||
|
||||
# defining image ratios
|
||||
resolution_ratio = (
|
||||
(float(resolution_width) * pixel_aspect) / resolution_height
|
||||
)
|
||||
delivery_ratio = float(self.to_width) / float(self.to_height)
|
||||
self.log.debug("resolution_ratio: `{}`".format(resolution_ratio))
|
||||
self.log.debug("delivery_ratio: `{}`".format(delivery_ratio))
|
||||
|
||||
# shorten two decimals long float number for testing conditions
|
||||
resolution_ratio_test = float("{:0.2f}".format(resolution_ratio))
|
||||
delivery_ratio_test = float("{:0.2f}".format(delivery_ratio))
|
||||
|
||||
# get scale factor
|
||||
if resolution_ratio_test < delivery_ratio_test:
|
||||
scale_factor = (
|
||||
float(self.to_width) / (resolution_width * pixel_aspect)
|
||||
)
|
||||
else:
|
||||
scale_factor = (
|
||||
float(self.to_height) / (resolution_height * pixel_aspect)
|
||||
)
|
||||
|
||||
self.log.debug("__ scale_factor: `{}`".format(scale_factor))
|
||||
|
||||
filters = []
|
||||
# letter_box
|
||||
if output_letterbox:
|
||||
ffmpeg_width = self.to_width
|
||||
ffmpeg_height = self.to_height
|
||||
if "reformat" not in repre["tags"]:
|
||||
output_letterbox /= pixel_aspect
|
||||
if resolution_ratio_test != delivery_ratio_test:
|
||||
ffmpeg_width = resolution_width
|
||||
ffmpeg_height = int(
|
||||
resolution_height * pixel_aspect)
|
||||
else:
|
||||
if resolution_ratio_test != delivery_ratio_test:
|
||||
output_letterbox /= scale_factor
|
||||
else:
|
||||
output_letterbox /= pixel_aspect
|
||||
|
||||
filters.append(
|
||||
"scale={}x{}:flags=lanczos".format(ffmpeg_width, ffmpeg_height)
|
||||
)
|
||||
# QUESTION shouldn't this contain aspect ration value instead of 1?
|
||||
filters.append(
|
||||
"setsar=1"
|
||||
)
|
||||
filters.append((
|
||||
"drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t=fill:c=black"
|
||||
).format(output_letterbox))
|
||||
|
||||
filters.append((
|
||||
"drawbox=0:ih-round((ih-(iw*(1/{0})))/2)"
|
||||
":iw:round((ih-(iw*(1/{0})))/2):t=fill:c=black"
|
||||
).format(output_letterbox))
|
||||
|
||||
self.log.debug("pixel_aspect: `{}`".format(pixel_aspect))
|
||||
self.log.debug("resolution_width: `{}`".format(resolution_width))
|
||||
self.log.debug("resolution_height: `{}`".format(resolution_height))
|
||||
|
||||
# scaling none square pixels and 1920 width
|
||||
# QUESTION: again check only output tags or repre tags
|
||||
# WARNING: Duplication of filters when letter_box is set (or not?)
|
||||
if "reformat" in repre["tags"]:
|
||||
if resolution_ratio_test < delivery_ratio_test:
|
||||
self.log.debug("lower then delivery")
|
||||
width_scale = int(self.to_width * scale_factor)
|
||||
width_half_pad = int((self.to_width - width_scale) / 2)
|
||||
height_scale = self.to_height
|
||||
height_half_pad = 0
|
||||
else:
|
||||
self.log.debug("heigher then delivery")
|
||||
width_scale = self.to_width
|
||||
width_half_pad = 0
|
||||
scale_factor = (
|
||||
float(self.to_width)
|
||||
/ (float(resolution_width) * pixel_aspect)
|
||||
)
|
||||
self.log.debug(
|
||||
"__ scale_factor: `{}`".format(scale_factor)
|
||||
)
|
||||
height_scale = int(resolution_height * scale_factor)
|
||||
height_half_pad = int((self.to_height - height_scale) / 2)
|
||||
|
||||
self.log.debug("width_scale: `{}`".format(width_scale))
|
||||
self.log.debug("width_half_pad: `{}`".format(width_half_pad))
|
||||
self.log.debug("height_scale: `{}`".format(height_scale))
|
||||
self.log.debug("height_half_pad: `{}`".format(height_half_pad))
|
||||
|
||||
filters.append(
|
||||
"scale={}x{}:flags=lanczos".format(width_scale, height_scale)
|
||||
)
|
||||
filters.append(
|
||||
"pad={}:{}:{}:{}:black".format(
|
||||
self.to_width, self.to_height,
|
||||
width_half_pad,
|
||||
height_half_pad
|
||||
)
|
||||
filters.append("setsar=1")
|
||||
|
||||
return filters
|
||||
|
||||
def main_family_from_instance(self, instance):
|
||||
family = instance.data.get("family")
|
||||
if not family:
|
||||
|
|
@ -517,9 +718,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
return filtered_outputs
|
||||
|
||||
def filter_outputs_by_tags(self, outputs, tags):
|
||||
filtered_outputs = {}
|
||||
filtered_outputs = []
|
||||
repre_tags_low = [tag.lower() for tag in tags]
|
||||
for filename_suffix, output_def in outputs.items():
|
||||
for output_def in outputs:
|
||||
valid = True
|
||||
output_filters = output_def.get("output_filter")
|
||||
if output_filters:
|
||||
|
|
@ -537,7 +738,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
continue
|
||||
|
||||
if valid:
|
||||
filtered_outputs[filename_suffix] = output_def
|
||||
filtered_outputs.append(output_def)
|
||||
|
||||
return filtered_outputs
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue