Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Add timecode to slate #2929

Merged
merged 37 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
75838df
Handle timecode and audio state
Mar 22, 2022
2dd38c7
fix hound
Mar 22, 2022
c67a550
Fix Hound2
Mar 22, 2022
406575e
Hound3
Mar 22, 2022
16817c3
remove no-audio tag settings
jrsndl Mar 22, 2022
83a690d
Merge branch 'develop' into bugfix/OP-2913-Nuke-Slate-no-timecode
jakubjezek001 Mar 22, 2022
fac7ebd
Merge branch 'develop' into bugfix/OP-2913-Nuke-Slate-no-timecode
jakubjezek001 Mar 22, 2022
f866bf2
adding back `schema_representation_tags`
jakubjezek001 Mar 22, 2022
ef0210a
Error Handling, ffmpeg fixes
Mar 28, 2022
0270817
hound
Mar 28, 2022
90e29c6
hound2
Mar 28, 2022
13f6b03
hound3
Mar 28, 2022
0db0fa0
stray empty file?
jrsndl Mar 29, 2022
19e14c5
Merge branch 'develop' into bugfix/OP-2913-Nuke-Slate-no-timecode
jrsndl Apr 14, 2022
5dadfb2
fix merge conflict
jrsndl Apr 14, 2022
8abc3ff
hound
jrsndl Apr 14, 2022
0962a55
Merge branch 'develop' into bugfix/OP-2913-Nuke-Slate-no-timecode
jakubjezek001 Apr 26, 2022
f1168b1
Merge branch 'develop' into bugfix/OP-2913-Nuke-Slate-no-timecode
jakubjezek001 Apr 26, 2022
faff541
Removed submodule repos/avalon-core
jakubjezek001 Apr 26, 2022
fb2327a
concat timecode fix
Apr 29, 2022
5d8cea5
hound
Apr 29, 2022
4972578
Fix concatenating metadata
Apr 29, 2022
a80d378
hound
Apr 29, 2022
19b6cb7
concatenation output formats fix
May 5, 2022
2e7c823
mad dog fix
May 5, 2022
fe514cf
more fixes
May 5, 2022
de31b8d
add silent audio to slate
May 10, 2022
b0c9e8a
hound
May 10, 2022
ba1587e
safer code
May 11, 2022
82e6223
int back
May 11, 2022
a176228
Update openpype/plugins/publish/extract_review_slate.py
jrsndl May 23, 2022
bf80eee
Update openpype/plugins/publish/extract_review_slate.py
jrsndl May 23, 2022
73a0d6f
extracted some functionality into separated functions
iLLiCiTiT May 25, 2022
4c3914c
fix double space
iLLiCiTiT May 25, 2022
951bbb6
Merge branch 'bugfix/OP-2913-Nuke-Slate-no-timecode' into Slate-with-…
iLLiCiTiT May 31, 2022
2effd17
Merge pull request #3162 from pypeclub/Slate-with-audio
iLLiCiTiT May 31, 2022
9867f00
Merge branch 'develop' into bugfix/OP-2913-Nuke-Slate-no-timecode
iLLiCiTiT May 31, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 251 additions & 29 deletions openpype/plugins/publish/extract_review_slate.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def process(self, instance):

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))
Expand All @@ -73,27 +74,31 @@ def process(self, instance):
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:
raise AssertionError((
"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"]
Expand Down Expand Up @@ -149,14 +154,27 @@ def process(self, instance):
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', [])
Expand All @@ -171,10 +189,10 @@ def process(self, instance):

# 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
Expand Down Expand Up @@ -210,15 +228,24 @@ def process(self, instance):
"__ 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")
Expand All @@ -244,6 +271,25 @@ def process(self, instance):
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(
Expand All @@ -269,12 +315,27 @@ def process(self, instance):
"-i", conc_text_path,
"-c", "copy",
]
if offset_timecode:
concat_args.extend(["-timecode", offset_timecode])
# NOTE: Added because of OP Atom demuxers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is required to add format definitions into contatenation otherwise mxf demuxer will use default mxf format (OP1a) to output.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jrsndl Remainding: With removing of these lines the output won't be OP Atom but OPa1. Concatenation also requires to specify output format otherwise default format option is used..

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I had this already fixed. Oh well, Git challenged...

# 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
Expand Down Expand Up @@ -309,6 +370,129 @@ def process(self, instance):

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
Expand Down Expand Up @@ -375,3 +559,41 @@ def _get_format_codec_args(self, repre):
)

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
1 change: 1 addition & 0 deletions openpype/scripts/otio_burnin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down