mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #2929 from pypeclub/bugfix/OP-2913-Nuke-Slate-no-timecode
Add timecode to slate
This commit is contained in:
commit
918a9cbb26
2 changed files with 252 additions and 29 deletions
|
|
@ -58,6 +58,7 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
|
||||
pixel_aspect = inst_data.get("pixelAspect", 1)
|
||||
fps = inst_data.get("fps")
|
||||
self.log.debug("fps {} ".format(fps))
|
||||
|
||||
for idx, repre in enumerate(inst_data["representations"]):
|
||||
self.log.debug("repre ({}): `{}`".format(idx + 1, repre))
|
||||
|
|
@ -73,20 +74,16 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
os.path.normpath(stagingdir), repre["files"])
|
||||
self.log.debug("__ input_path: {}".format(input_path))
|
||||
|
||||
video_streams = get_ffprobe_streams(
|
||||
streams = get_ffprobe_streams(
|
||||
input_path, self.log
|
||||
)
|
||||
|
||||
# Try to find first stream with defined 'width' and 'height'
|
||||
# - this is to avoid order of streams where audio can be as first
|
||||
# - there may be a better way (checking `codec_type`?)
|
||||
input_width = None
|
||||
input_height = None
|
||||
for stream in video_streams:
|
||||
if "width" in stream and "height" in stream:
|
||||
input_width = int(stream["width"])
|
||||
input_height = int(stream["height"])
|
||||
break
|
||||
# Get video metadata
|
||||
(
|
||||
input_width,
|
||||
input_height,
|
||||
input_timecode,
|
||||
input_frame_rate
|
||||
) = self._get_video_metadata(streams)
|
||||
|
||||
# Raise exception of any stream didn't define input resolution
|
||||
if input_width is None:
|
||||
|
|
@ -94,6 +91,14 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
"FFprobe couldn't read resolution from input file: \"{}\""
|
||||
).format(input_path))
|
||||
|
||||
(
|
||||
audio_codec,
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
audio_channel_layout,
|
||||
input_audio
|
||||
) = self._get_audio_metadata(streams)
|
||||
|
||||
# values are set in ExtractReview
|
||||
if use_legacy_code:
|
||||
to_width = inst_data["reviewToWidth"]
|
||||
|
|
@ -149,14 +154,27 @@ 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(
|
||||
path_to_subprocess_arg(slate_path)
|
||||
))
|
||||
|
||||
input_args.extend([
|
||||
"-r {}".format(fps),
|
||||
"-t 0.04"
|
||||
"-loop", "1",
|
||||
"-i", openpype.lib.path_to_subprocess_arg(slate_path),
|
||||
"-r", str(input_frame_rate),
|
||||
"-frames:v", "1",
|
||||
])
|
||||
|
||||
# add timecode from source to the slate, substract one frame
|
||||
offset_timecode = ""
|
||||
if input_timecode:
|
||||
offset_timecode = self._tc_offset(
|
||||
str(input_timecode),
|
||||
framerate=fps,
|
||||
frame_offset=-1
|
||||
)
|
||||
self.log.debug("Slate Timecode: `{}`".format(
|
||||
offset_timecode
|
||||
))
|
||||
input_args.extend(["-timecode", str(offset_timecode)])
|
||||
|
||||
if use_legacy_code:
|
||||
format_args = []
|
||||
codec_args = repre["_profile"].get('codec', [])
|
||||
|
|
@ -171,10 +189,10 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
|
||||
# make sure colors are correct
|
||||
output_args.extend([
|
||||
"-vf scale=out_color_matrix=bt709",
|
||||
"-color_primaries bt709",
|
||||
"-color_trc bt709",
|
||||
"-colorspace bt709"
|
||||
"-vf", "scale=out_color_matrix=bt709",
|
||||
"-color_primaries", "bt709",
|
||||
"-color_trc", "bt709",
|
||||
"-colorspace", "bt709",
|
||||
])
|
||||
|
||||
# scaling none square pixels and 1920 width
|
||||
|
|
@ -210,15 +228,24 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
"__ height_half_pad: `{}`".format(height_half_pad)
|
||||
)
|
||||
|
||||
scaling_arg = ("scale={0}x{1}:flags=lanczos,"
|
||||
"pad={2}:{3}:{4}:{5}:black,setsar=1").format(
|
||||
width_scale, height_scale, to_width, to_height,
|
||||
width_half_pad, height_half_pad
|
||||
scaling_arg = (
|
||||
"scale={0}x{1}:flags=lanczos"
|
||||
",pad={2}:{3}:{4}:{5}:black"
|
||||
",setsar=1"
|
||||
",fps={6}"
|
||||
).format(
|
||||
width_scale,
|
||||
height_scale,
|
||||
to_width,
|
||||
to_height,
|
||||
width_half_pad,
|
||||
height_half_pad,
|
||||
input_frame_rate
|
||||
)
|
||||
|
||||
vf_back = self.add_video_filter_args(output_args, scaling_arg)
|
||||
# add it to output_args
|
||||
output_args.insert(0, vf_back)
|
||||
vf_back = self.add_video_filter_args(output_args, scaling_arg)
|
||||
# add it to output_args
|
||||
output_args.insert(0, vf_back)
|
||||
|
||||
# overrides output file
|
||||
output_args.append("-y")
|
||||
|
|
@ -244,6 +271,25 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
slate_subprocess_cmd, shell=True, logger=self.log
|
||||
)
|
||||
|
||||
# Create slate with silent audio track
|
||||
if input_audio:
|
||||
# silent slate output path
|
||||
slate_silent_path = "_silent".join(
|
||||
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,
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
audio_channel_layout,
|
||||
)
|
||||
|
||||
# replace slate with silent slate for concat
|
||||
slate_v_path = slate_silent_path
|
||||
|
||||
# create ffmpeg concat text file path
|
||||
conc_text_file = input_file.replace(ext, "") + "_concat" + ".txt"
|
||||
conc_text_path = os.path.join(
|
||||
|
|
@ -269,12 +315,27 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
"-i", conc_text_path,
|
||||
"-c", "copy",
|
||||
]
|
||||
if offset_timecode:
|
||||
concat_args.extend(["-timecode", offset_timecode])
|
||||
# NOTE: Added because of OP Atom demuxers
|
||||
# Add format arguments if there are any
|
||||
# - keep format of output
|
||||
if format_args:
|
||||
concat_args.extend(format_args)
|
||||
# Add final output path
|
||||
# Use arguments from ffmpeg preset
|
||||
source_ffmpeg_cmd = repre.get("ffmpeg_cmd")
|
||||
if source_ffmpeg_cmd:
|
||||
copy_args = (
|
||||
"-metadata",
|
||||
"-metadata:s:v:0",
|
||||
)
|
||||
args = source_ffmpeg_cmd.split(" ")
|
||||
for indx, arg in enumerate(args):
|
||||
if arg in copy_args:
|
||||
concat_args.append(arg)
|
||||
# assumes arg has one parameter
|
||||
concat_args.append(args[indx + 1])
|
||||
# add final output path
|
||||
concat_args.append(output_path)
|
||||
|
||||
# ffmpeg concat subprocess
|
||||
|
|
@ -309,6 +370,129 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
|
||||
self.log.debug(inst_data["representations"])
|
||||
|
||||
def _get_video_metadata(self, streams):
|
||||
input_timecode = ""
|
||||
input_width = None
|
||||
input_height = None
|
||||
input_frame_rate = None
|
||||
for stream in streams:
|
||||
if stream.get("codec_type") != "video":
|
||||
continue
|
||||
self.log.debug("FFprobe Video: {}".format(stream))
|
||||
|
||||
if "width" not in stream or "height" not in stream:
|
||||
continue
|
||||
width = int(stream["width"])
|
||||
height = int(stream["height"])
|
||||
if not width or not height:
|
||||
continue
|
||||
|
||||
# Make sure that width and height are captured even if frame rate
|
||||
# is not available
|
||||
input_width = width
|
||||
input_height = height
|
||||
|
||||
tags = stream.get("tags") or {}
|
||||
input_timecode = tags.get("timecode") or ""
|
||||
|
||||
input_frame_rate = stream.get("r_frame_rate")
|
||||
if input_frame_rate is not None:
|
||||
break
|
||||
return (
|
||||
input_width,
|
||||
input_height,
|
||||
input_timecode,
|
||||
input_frame_rate
|
||||
)
|
||||
|
||||
def _get_audio_metadata(self, streams):
|
||||
# Get audio metadata
|
||||
audio_codec = None
|
||||
audio_channels = None
|
||||
audio_sample_rate = None
|
||||
audio_channel_layout = None
|
||||
input_audio = False
|
||||
|
||||
for stream in streams:
|
||||
if stream.get("codec_type") != "audio":
|
||||
continue
|
||||
self.log.debug("__Ffprobe Audio: {}".format(stream))
|
||||
|
||||
if all(
|
||||
stream.get(key)
|
||||
for key in (
|
||||
"codec_name",
|
||||
"channels",
|
||||
"sample_rate",
|
||||
"channel_layout",
|
||||
)
|
||||
):
|
||||
audio_codec = stream["codec_name"]
|
||||
audio_channels = stream["channels"]
|
||||
audio_sample_rate = stream["sample_rate"]
|
||||
audio_channel_layout = stream["channel_layout"]
|
||||
input_audio = True
|
||||
break
|
||||
|
||||
return (
|
||||
audio_codec,
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
audio_channel_layout,
|
||||
input_audio,
|
||||
)
|
||||
|
||||
def _create_silent_slate(
|
||||
self,
|
||||
ffmpeg_path,
|
||||
src_path,
|
||||
dst_path,
|
||||
audio_codec,
|
||||
audio_channels,
|
||||
audio_sample_rate,
|
||||
audio_channel_layout,
|
||||
):
|
||||
# Get duration of one frame in micro seconds
|
||||
items = audio_sample_rate.split("/")
|
||||
if len(items) == 1:
|
||||
one_frame_duration = 1.0 / float(items[0])
|
||||
elif len(items) == 2:
|
||||
one_frame_duration = float(items[1]) / float(items[0])
|
||||
else:
|
||||
one_frame_duration = None
|
||||
|
||||
if one_frame_duration is None:
|
||||
one_frame_duration = "40000us"
|
||||
else:
|
||||
one_frame_duration *= 1000000
|
||||
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,
|
||||
"-i", src_path,
|
||||
"-f", "lavfi", "-i",
|
||||
"anullsrc=r={}:cl={}:d={}".format(
|
||||
audio_sample_rate,
|
||||
audio_channel_layout,
|
||||
one_frame_duration
|
||||
),
|
||||
"-c:v", "copy",
|
||||
"-c:a", audio_codec,
|
||||
"-map", "0:v",
|
||||
"-map", "1:a",
|
||||
"-shortest",
|
||||
"-y",
|
||||
dst_path
|
||||
]
|
||||
# run slate generation subprocess
|
||||
self.log.debug("Silent Slate Executing: {}".format(
|
||||
" ".join(slate_silent_args)
|
||||
))
|
||||
openpype.api.run_subprocess(
|
||||
slate_silent_args, logger=self.log
|
||||
)
|
||||
|
||||
def add_video_filter_args(self, args, inserting_arg):
|
||||
"""
|
||||
Fixing video filter argumets to be one long string
|
||||
|
|
@ -375,3 +559,41 @@ class ExtractReviewSlate(openpype.api.Extractor):
|
|||
)
|
||||
|
||||
return format_args, codec_args
|
||||
|
||||
def _tc_offset(self, timecode, framerate=24.0, frame_offset=-1):
|
||||
"""Offsets timecode by frame"""
|
||||
def _seconds(value, framerate):
|
||||
if isinstance(value, str):
|
||||
_zip_ft = zip((3600, 60, 1, 1 / framerate), value.split(':'))
|
||||
_s = sum(f * float(t) for f, t in _zip_ft)
|
||||
elif isinstance(value, (int, float)):
|
||||
_s = value / framerate
|
||||
else:
|
||||
_s = 0
|
||||
return _s
|
||||
|
||||
def _frames(seconds, framerate, frame_offset):
|
||||
_f = seconds * framerate + frame_offset
|
||||
if _f < 0:
|
||||
_f = framerate * 60 * 60 * 24 + _f
|
||||
return _f
|
||||
|
||||
def _timecode(seconds, framerate):
|
||||
return '{h:02d}:{m:02d}:{s:02d}:{f:02d}'.format(
|
||||
h=int(seconds / 3600),
|
||||
m=int(seconds / 60 % 60),
|
||||
s=int(seconds % 60),
|
||||
f=int(round((seconds - int(seconds)) * framerate)))
|
||||
drop = False
|
||||
if ';' in timecode:
|
||||
timecode = timecode.replace(';', ':')
|
||||
drop = True
|
||||
frames = _frames(
|
||||
_seconds(timecode, framerate),
|
||||
framerate,
|
||||
frame_offset
|
||||
)
|
||||
tc = _timecode(_seconds(frames, framerate), framerate)
|
||||
if drop:
|
||||
tc = ';'.join(tc.rsplit(':', 1))
|
||||
return tc
|
||||
|
|
|
|||
|
|
@ -568,6 +568,7 @@ def burnins_from_data(
|
|||
if source_ffmpeg_cmd:
|
||||
copy_args = (
|
||||
"-metadata",
|
||||
"-metadata:s:v:0",
|
||||
)
|
||||
args = source_ffmpeg_cmd.split(" ")
|
||||
for idx, arg in enumerate(args):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue