Skip to content

Commit

Permalink
Support video color depth of 10 bits
Browse files Browse the repository at this point in the history
  • Loading branch information
double16 committed Sep 27, 2023
1 parent fb6b594 commit 6601595
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 25 deletions.
5 changes: 3 additions & 2 deletions dvrprocess/comcut.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ def comcut(infile, outfile, delete_edl=True, force_clear_edl=False, delete_meta=

video_info = common.find_video_stream(input_info)
height = common.get_video_height(video_info)
depth = common.get_video_depth(video_info)
target_video_codec = common.resolve_video_codec(desired_video_codecs, height, video_info)

if len(video_filters) > 0 or len(keyframes) == 0 or force_encode:
Expand Down Expand Up @@ -417,7 +418,7 @@ def comcut(infile, outfile, delete_edl=True, force_clear_edl=False, delete_meta=
for stream in common.sort_streams(input_info[constants.K_STREAMS]):
if common.is_video_stream(stream) and (len(video_filters) > 0 or len(keyframes) == 0 or force_encode):
height = common.get_video_height(stream)
crf, bitrate, qp = common.recommended_video_quality(height, target_video_codec)
crf, bitrate, qp = common.recommended_video_quality(height, target_video_codec, depth)

# adjust frame rate
desired_frame_rate = config.get_global_config_frame_rate('post_process', 'frame_rate', None)
Expand All @@ -431,7 +432,7 @@ def comcut(infile, outfile, delete_edl=True, force_clear_edl=False, delete_meta=
encoding_options, encoding_method = hwaccel.hwaccel_encoding(output_stream=str(output_stream_idx),
codec=target_video_codec, output_type="mkv",
tune=None, preset=preset, crf=crf, qp=qp,
target_bitrate=bitrate)
target_bitrate=bitrate, bit_depth=depth)

# add common video filters if we are doing filtering
video_filters.append('format=nv12')
Expand Down
22 changes: 19 additions & 3 deletions dvrprocess/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def find_input_info(filename):
return input_info


def get_video_height(video_info) -> [None, int]:
def get_video_height(video_info: dict) -> [None, int]:
"""
Get the height of the video considering symbolic names like 'sd'.
:param video_info: can be all of the input_info to use default video stream, or a single video stream
Expand All @@ -297,7 +297,7 @@ def get_video_height(video_info) -> [None, int]:
return height


def get_video_width(video_info) -> [None, int]:
def get_video_width(video_info: dict) -> [None, int]:
"""
Get the width of the video considering symbolic names.
:param video_info: can be all of the input_info to use default video stream, or a single video stream
Expand All @@ -311,6 +311,22 @@ def get_video_width(video_info) -> [None, int]:
return width


def get_video_depth(video_info: dict) -> [None, int]:
"""
Get the bit depth of the video.
:param video_info: can be all of the input_info to use default video stream, or a single video stream
:return: int: 8, 10, etc. or None
"""
if 'streams' in video_info:
video_info = find_video_stream(video_info)
profile = video_info.get('profile', None)
if not profile:
return None
if '10' in profile:
return 10
return 8


def get_frame_rate(video_info):
"""
Get the frame rate of the video.
Expand Down Expand Up @@ -657,7 +673,7 @@ def is_codec_available(codec: str) -> bool:
return True


def recommended_video_quality(target_height: int, target_video_codec: str) -> (int, int, int):
def recommended_video_quality(target_height: int, target_video_codec: str, bit_depth: Union[int, None]) -> (int, int, int):
"""
CRF
https://slhck.info/video/2017/02/24/crf-guide.html
Expand Down
2 changes: 1 addition & 1 deletion dvrprocess/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def find_config(filename: str) -> str:
"""
sources = _get_config_sources(filename)
for f in sources:
if os.access(f, os.R_OK):
if os.path.isfile(f) and os.access(f, os.R_OK):
return f
raise OSError(f"Cannot find {filename} in any of {','.join(sources)}")

Expand Down
48 changes: 34 additions & 14 deletions dvrprocess/common/hwaccel.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,7 @@ def hwaccel_required_hwupload_filter() -> bool:


def hwaccel_encoding(output_stream: str, codec: str, output_type: str, tune: [None, str], preset: str, crf: int,
qp: int,
target_bitrate: int) -> (list[str], HWAccelMethod):
qp: int, target_bitrate: int, bit_depth: Union[int, None]) -> (list[str], HWAccelMethod):
"""
Return ffmpeg options for using hwaccel for encoding.
:param output_stream: ffmpeg output stream spec, '1', ...
Expand All @@ -321,6 +320,7 @@ def hwaccel_encoding(output_stream: str, codec: str, output_type: str, tune: [No
:param crf: 23, 31, ...
:param qp: 28, 34, ...
:param target_bitrate: in kbps, such as 1200, 3500, ...
:param bit_depth: color bit depth: 8, 10, None for default selection
"""
if preset == "copy":
return [f"-c:{output_stream}", "copy"], HWAccelMethod.NONE
Expand All @@ -329,24 +329,27 @@ def hwaccel_encoding(output_stream: str, codec: str, output_type: str, tune: [No
if hwaccel_requested in [HWAccelRequest.FULL,
HWAccelRequest.NVENC] and method == HWAccelMethod.NVENC and has_hw_codec(codec):
return (_nvenc_encoding(output_stream=output_stream, codec=codec, output_type=output_type, tune=tune,
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate), HWAccelMethod.NVENC)
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate, bit_depth=bit_depth),
HWAccelMethod.NVENC)
elif hwaccel_requested in [HWAccelRequest.FULL,
HWAccelRequest.VAAPI] and method == HWAccelMethod.VAAPI and has_hw_codec(codec):
return (_vaapi_encoding(output_stream=output_stream, codec=codec, output_type=output_type, tune=tune,
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate), HWAccelMethod.VAAPI)
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate, bit_depth=bit_depth),
HWAccelMethod.VAAPI)
elif hwaccel_requested in [HWAccelRequest.FULL,
HWAccelRequest.VIDEO_TOOLBOX] and method == HWAccelMethod.VIDEO_TOOLBOX and has_hw_codec(
codec):
return (_video_toolbox_encoding(output_stream=output_stream, codec=codec, output_type=output_type, tune=tune,
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate),
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate, bit_depth=bit_depth),
HWAccelMethod.VIDEO_TOOLBOX)
else:
return (_sw_encoding(output_stream=output_stream, codec=codec, output_type=output_type, tune=tune,
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate), HWAccelMethod.NONE)
preset=preset, crf=crf, qp=qp, target_bitrate=target_bitrate, bit_depth=bit_depth),
HWAccelMethod.NONE)


def _nvenc_encoding(output_stream: str, codec: str, output_type: str, tune: str, preset: str, crf: int, qp: int,
target_bitrate: int):
target_bitrate: int, bit_depth: Union[int, None]):
# https://docs.nvidia.com/video-technologies/video-codec-sdk/nvenc-video-encoder-api-prog-guide/

options = [f"-c:{output_stream}"]
Expand Down Expand Up @@ -400,20 +403,24 @@ def _nvenc_encoding(output_stream: str, codec: str, output_type: str, tune: str,
if codec in ['h264']:
options.extend((f"-profile:{output_stream}", "high"))
elif codec in ['h265', 'hevc']:
# TODO: if 10-bit depth, use 'main-10'
options.extend((f"-profile:{output_stream}", "main"))
if bit_depth and bit_depth >= 10:
options.extend((f"-profile:{output_stream}", "main10"))
else:
options.extend((f"-profile:{output_stream}", "main"))

return options


def _vaapi_encoding(output_stream: str, codec: str, output_type: str, tune: str, preset: str, crf: int, qp: int,
target_bitrate: int):
target_bitrate: int, bit_depth: Union[int, None]):
options = [f"-c:{output_stream}"]
if codec in ['h265']:
options.extend(["hevc_vaapi"])
else:
options.extend([f"{codec}_vaapi"])

options.extend(["-compression_level", "1"])

if codec in ['h264', 'h265', 'hevc']:
options.extend([f"-rc_mode:{output_stream}", "VBR",
f"-qp:{output_stream}", str(qp),
Expand All @@ -424,12 +431,17 @@ def _vaapi_encoding(output_stream: str, codec: str, output_type: str, tune: str,
if codec in ['h264']:
options.extend([f"-profile:{output_stream}", "high"])
options.extend([f"-quality:{output_stream}", "0"])
elif codec in ['h265', 'hevc']:
if bit_depth and bit_depth >= 10:
options.extend((f"-profile:{output_stream}", "main10"))
else:
options.extend((f"-profile:{output_stream}", "main"))

return options


def _video_toolbox_encoding(output_stream: str, codec: str, output_type: str, tune: str, preset: str, crf: int, qp: int,
target_bitrate: int):
target_bitrate: int, bit_depth: Union[int, None]):
options = [f"-c:{output_stream}"]
if codec in ['h265']:
options.extend(["hevc_videotoolbox"])
Expand All @@ -446,15 +458,19 @@ def _video_toolbox_encoding(output_stream: str, codec: str, output_type: str, tu
if output_type != 'ts':
options.extend([f'-a53cc:{output_stream}', 'false'])

if codec == 'h265':
if codec in ['h265', 'hevc']:
if bit_depth and bit_depth >= 10:
options.extend((f"-profile:{output_stream}", "main10"))
else:
options.extend((f"-profile:{output_stream}", "main"))
# https://trac.ffmpeg.org/wiki/Encode/H.265
options.extend([f'-tag:{output_stream}', 'hvc1'])

return options


def _sw_encoding(output_stream: str, codec: str, output_type: str, tune: str, preset: str, crf: int, qp: int,
target_bitrate: int):
target_bitrate: int, bit_depth: Union[int, None]):
options = [f"-c:{output_stream}", ffmpeg_sw_codec(codec),
f"-crf:{output_stream}", str(crf),
f"-preset:{output_stream}", preset]
Expand All @@ -463,7 +479,11 @@ def _sw_encoding(output_stream: str, codec: str, output_type: str, tune: str, pr
# Do not copy Closed Captions, they will be extracted into a subtitle stream
if codec == 'h264' and output_type != 'ts':
options.extend([f"-a53cc:{output_stream}", '0'])
if codec == 'h265':
if codec in ['h265', 'hevc']:
if bit_depth and bit_depth >= 10:
options.extend((f"-profile:{output_stream}", "main10"))
else:
options.extend((f"-profile:{output_stream}", "main"))
# https://trac.ffmpeg.org/wiki/Encode/H.265
options.extend([f'-tag:{output_stream}', 'hvc1'])
return options
Expand Down
8 changes: 5 additions & 3 deletions dvrprocess/common/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ def __init__(self):
self.time_matcher = re.compile(r'\btime=\s*([0-9][0-9]:[0-9][0-9]:[0-9][0-9])')

def _run(self, arguments: list[str], kwargs) -> int:
if kwargs.get('capture_output', None) is True or kwargs.get('stderr', None) in [subprocess.PIPE, subprocess.STDOUT]:
if kwargs.get('capture_output', None) is True or kwargs.get('stderr', None) in [subprocess.PIPE,
subprocess.STDOUT]:
return super()._run(arguments, kwargs)

task_name = None
for idx, arg in enumerate(arguments):
if arg == '-i' and (task_name is None or '.mkv' in arguments[idx+1]):
task_name = os.path.basename(arguments[idx+1]) + ' ' + self.command_basename
if arg == '-i' and (task_name is None or '.mkv' in arguments[idx + 1]):
task_name = os.path.basename(arguments[idx + 1]) + ' ' + self.command_basename
if task_name is None:
task_name = self.command_basename

Expand Down Expand Up @@ -73,6 +74,7 @@ def _run(self, arguments: list[str], kwargs) -> int:

ffmpeg = FFmpegProcInvoker()


def _ffprobe_version_parser(path):
_maybe_version = float(
re.search(r"version (\d+[.]\d+)", subprocess.check_output([path, '-version'], text=True))[1])
Expand Down
5 changes: 3 additions & 2 deletions dvrprocess/dvr_post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def do_dvr_post_process(input_file,
if not width:
logger.error(f"Could not get width info from {filename}: {video_info}")
return 255
depth = common.get_video_depth(video_info)
frame_rate = common.get_frame_rate(video_info)
input_video_codec = common.resolve_video_codec(video_info['codec_name'])

Expand Down Expand Up @@ -342,7 +343,7 @@ def do_dvr_post_process(input_file,
adjust_frame_rate = common.should_adjust_frame_rate(current_frame_rate=frame_rate,
desired_frame_rate=desired_frame_rate, tolerance=0.05)

crf, bitrate, qp = common.recommended_video_quality(target_height, target_video_codec)
crf, bitrate, qp = common.recommended_video_quality(target_height, target_video_codec, depth)

# h264_vaapi wasn't a good idea, the bitrate is much higher than software encoding
# check if either bitrate is too high or vaapi encoding and re-encode
Expand Down Expand Up @@ -521,7 +522,7 @@ def do_dvr_post_process(input_file,
encoding_options, encoding_method = hwaccel.hwaccel_encoding(output_stream=str(current_output_stream),
codec=target_video_codec, output_type=output_type,
tune=tune, preset=preset, crf=crf, qp=qp,
target_bitrate=bitrate)
target_bitrate=bitrate, bit_depth=depth)
video_encoder_options_tag_value.extend(encoding_options)

filter_stage = 0
Expand Down

0 comments on commit 6601595

Please sign in to comment.