From ba70c23ecb5b99d7b19aed14249015e86c716d83 Mon Sep 17 00:00:00 2001 From: Oleg Kravchenko Date: Tue, 23 Jul 2024 10:13:33 +0300 Subject: [PATCH] refactor filters and encoders --- cpmyvideos.py | 30 ++++++++++++------------------ enc_dnxhr.py | 10 +++++++--- enc_hevc_amf.py | 11 +++++++---- enc_hevc_nv.py | 29 +++++++++++++++++++---------- enc_hevc_nvenc.py | 12 ++++++++++-- enc_hevc_vaapi.py | 31 +++++++++++++++++++------------ enc_hevc_vceenc.py | 8 ++++++-- enc_hevc_x265.py | 11 +++++++---- lib.py | 24 ++++++++++++++++++++++++ mymediainfo.py | 25 ++++++++++++++++--------- 10 files changed, 127 insertions(+), 64 deletions(-) diff --git a/cpmyvideos.py b/cpmyvideos.py index 170f27a..26c0696 100755 --- a/cpmyvideos.py +++ b/cpmyvideos.py @@ -17,10 +17,6 @@ RESOLUTIONS = (1080, 1440, 2160) FORMATS = ('dnxhr', 'hevc') -FORMAT_EXTENSIONS = { - 'hevc': 'MOV', - 'dnxhr': 'MOV', -} # downscale: Lanczos/Spline, upscale: Bicubic/Lanczos # error diffusion dithering to minimize banding +dither=error_diffusion DSCALE_FLAGS = 'flags=lanczos+accurate_rnd+full_chroma_int' @@ -95,14 +91,11 @@ def transcode(src, dst, info, enc_mod): else: cmd = ['ffmpeg', '-hide_banner', '-nostdin', '-ignore_editlist', '1'] - filter_v = [] - if hasattr(encoder, 'get_filter'): - filter_v.append(encoder.get_filter()) - if args.res and args.res < info.height: - if hasattr(encoder, 'scale'): - filter_v.append(encoder.scale()) - else: - filter_v.append(f'scale=w=-1:h={args.res}:{DSCALE_FLAGS}') + need_scale = args.res and args.res < info.height + filter_v = encoder.get_filter(scale=need_scale) + if need_scale and not encoder.can_scale: + filter_v.append({'scale': f'w=-1:h={args.res}:{DSCALE_FLAGS}'}) + #print(f'filter_v: {filter_v}') params = encoder.get_params() @@ -147,9 +140,12 @@ def transcode(src, dst, info, enc_mod): # filters if filter_v: if hasattr(encoder, 'CMD'): - cmd.extend(*filter_v) + cmd.extend(filter_v) else: - cmd.extend(['-filter:v', ','.join(filter_v)]) + items = lib.join_filters(filter_v) + #print(f'items: {items} from filter_v: {filter_v}') + cmd.extend(['-filter:v', items]) + #print(cmd) ; return 0 # output if args.duration: @@ -176,7 +172,6 @@ def copy(src, dst): ENC_MOD = enc_dnxhr elif args.fmt == 'hevc': ENC_MOD = importlib.import_module(f'enc_{args.fmt}_{args.enc}') - else: raise ValueError(f"Unsupported encoder format/type: {args.fmt}{args.enc}") @@ -197,7 +192,6 @@ def copy(src, dst): if args.res: debug.append(f'res: {args.res}') - ext = FORMAT_EXTENSIONS.get(args.fmt) base_name = os.path.splitext(filename)[0] if args.fnparams: if args.fmt == 'dnxhr': @@ -223,7 +217,7 @@ def copy(src, dst): base_name += f'_tun={args.tune}' if args.gop: base_name += f'_gop{lib.gop(mi.frame_rate, args.gop)}' - dst_file = os.path.join(args.dstdir, f'{base_name}.{ext}') + dst_file = os.path.join(args.dstdir, f'{base_name}.MOV') if os.path.exists(dst_file): print(f'EXISTS {dst_file}') if not args.dry: @@ -233,7 +227,7 @@ def copy(src, dst): debug.append(f'crf: {crf}') if args.preset: debug.append(f'preset: {args.preset}') - if args.fmt == 'dnxhr' and args.dnx: + if args.fmt == 'dnxhr' and dnxp: debug.append(f'DNxHR profile: {dnxp}') print('\n'.join(map(lambda s: f'> {s}', debug))) mi.print() diff --git a/enc_dnxhr.py b/enc_dnxhr.py index 8c341fe..8a0c645 100644 --- a/enc_dnxhr.py +++ b/enc_dnxhr.py @@ -7,6 +7,8 @@ Supported pixel formats: yuv422p yuv422p10le yuv444p10le gbrp10le """ +from lib import BaseEncoder + PROFILES = { 'lb': 'yuv422p', # Offline Quality. 22:1 'sq': 'yuv422p', # Suitable for delivery. 7:1 @@ -21,7 +23,9 @@ 'yuv422:10': 'hqx', } -class Encoder: +class Encoder(BaseEncoder): + + can_scale = False def __init__(self, vid): self.profile = vid.dnx if vid.dnx else PROFILES_AUTO[vid.idx()] @@ -36,5 +40,5 @@ def get_params(self): def get_profile(self): return self.profile - def get_filter(self): - return f'format={PROFILES[self.profile]}' + def get_filter(self, *args, scale=None, **kwargs): + return [{'format': PROFILES[self.profile]}] diff --git a/enc_hevc_amf.py b/enc_hevc_amf.py index 7a2d7b1..99b84d8 100644 --- a/enc_hevc_amf.py +++ b/enc_hevc_amf.py @@ -6,8 +6,11 @@ Supported pixel formats: nv12 yuv420p """ +from lib import BaseEncoder -class Encoder: +class Encoder(BaseEncoder): + + can_scale = False def __init__(self, vid): self.params = { @@ -18,7 +21,7 @@ def __init__(self, vid): 'qp_p': vid.crf, 'qp_i': vid.crf, 'profile_tier': 'high', - 'level': '5.2', + 'level': '5.1', } self.bits = vid.bits self.fmt = f'{vid.color_format}p' @@ -28,5 +31,5 @@ def __init__(self, vid): def get_params(self): return self.params - def get_filter(self): - return f'format={self.fmt}' + def get_filter(self, *args, scale=None, **kwargs): + return [{'format': self.fmt}] diff --git a/enc_hevc_nv.py b/enc_hevc_nv.py index 95ff904..c41d568 100644 --- a/enc_hevc_nv.py +++ b/enc_hevc_nv.py @@ -6,9 +6,12 @@ https://developer.nvidia.com/blog/calculating-video-quality-using-nvidia-gpus-and-vmaf-cuda/ https://developer.nvidia.com/blog/nvidia-ffmpeg-transcoding-guide/ +hwupload_cuda filter: uploading the data from system to GPU memory using + Supported pixel formats: yuv420p nv12 p010le yuv444p p016le yuv444p16le bgr0 bgra rgb0 rgba x2rgb10le x2bgr10le gbrp gbrp16le cuda """ +from lib import BaseEncoder PRESETS = { 1080: 'p6', @@ -19,7 +22,7 @@ DEFAULT_TUNE='hq' PARAMS_IN = { # vdpau cuda vaapi qsv drm opencl vulkan - 'hwaccel': 'nvdec', + 'hwaccel': 'cuda', # keeps the decoded frames in GPU memory 'hwaccel_output_format': 'cuda' } @@ -30,10 +33,11 @@ 'yuv422:10': 'yuv444p16le', } -class Encoder: +class Encoder(BaseEncoder): + can_scale = True def __init__(self, vid): - #print(f'hevc_nvenc idx:{vid.idx()}') + print(f'hevc_nvenc idx:{vid.idx()}') self.params = { 'c:v': 'hevc_nvenc', @@ -47,7 +51,7 @@ def __init__(self, vid): 'tier': 'high', 'profile:v': 'main10' if vid.bits == 10 else 'main', } - self.bits = vid.bits + self.bits_in = vid.bits_in self.res = vid.res self.idx = vid.idx() @@ -57,9 +61,14 @@ def get_params_in(self): def get_params(self): return self.params - def scale(self): - flt = ['hwupload_cuda'] - scale = f'scale_cuda=w=-1:h={self.res}:interp_algo=lanczos' - scale += f':format={FORMATS[self.idx]}' - flt.append(scale) - return ','.join(flt) + def get_filter(self, *args, scale=None, **kwargs): + flt = [] + # NVDec H.264/AVC: nv12, yv12 ONLY + if self.bits_in == 10: + flt.append('hwupload_cuda') + sparams = [] + if scale: + sparams.append(f'w=-1:h={self.res}:interp_algo=lanczos') + sparams.append(f'format={FORMATS[self.idx]}') + flt.append({'scale_cuda': ':'.join(sparams)}) + return flt diff --git a/enc_hevc_nvenc.py b/enc_hevc_nvenc.py index abab618..f8b5964 100644 --- a/enc_hevc_nvenc.py +++ b/enc_hevc_nvenc.py @@ -10,7 +10,12 @@ Max Level 186 (6.2) 4:4:4 yes 10bit depth yes + +NVDec features + H.264/AVC: nv12, yv12 + H.265/HEVC: nv12, yv12, yv12(10bit), yv12(12bit), yuv444, yuv444(10bit), yuv444(12bit) """ +from lib import BaseEncoder OUTPUT_BUFFER = 64 @@ -32,9 +37,10 @@ # smpte2084 bt2020nc bt2020c } -class Encoder: +class Encoder(BaseEncoder): CMD = ['nvencc'] + can_scale = True def __init__(self, vid): #print(f'nvenc idx: {vid.idx()}') @@ -64,5 +70,7 @@ def __init__(self, vid): def get_params(self): return self.params - def scale(self): + def get_filter(self, *args, scale=None, **kwargs): + if not scale: + return {} return ['--output-res', f'-2x{self.res}', '--vpp-resize', 'lanczos3'] diff --git a/enc_hevc_vaapi.py b/enc_hevc_vaapi.py index 9f7c0d9..f5a215b 100644 --- a/enc_hevc_vaapi.py +++ b/enc_hevc_vaapi.py @@ -8,6 +8,7 @@ Supported pixel formats: vaapi, scale_vaapi format nv12 yuv420p p010(420/10) yuy2(422/8) """ +from lib import BaseEncoder COMPLVL = 29 # 1 AMD # VBAQ=16 (not with CQP), pre-encode=8, quality=4, preset=2, speed=0 @@ -20,7 +21,8 @@ #'vaapi_device': '/dev/dri/renderD128', } -class Encoder: +class Encoder(BaseEncoder): + can_scale = True def __init__(self, vid): self.params = { @@ -40,15 +42,20 @@ def get_params_in(self): def get_params(self): return self.params - def get_filter(self): - flt = [] + def get_filter(self, *args, scale=None, **kwargs): + flt = ['hwupload'] + scale_vaapi = {} + + if scale: + scale_vaapi = { + 'w': -1, + 'h': self.res, + 'mode': 'hq', + 'force_original_aspect_ratio': 1 + } + if self.bits == 10: - flt.append('format=p010') - flt.append('hwupload') - return ','.join(flt) - - def scale(self): - flt = [] - if self.res: - flt.append(f'scale_vaapi=w=-1:h={self.res}:mode=hq:force_original_aspect_ratio=1') - return ','.join(flt) + scale_vaapi['format'] = 'p010' + + flt.append({'scale_vaapi': scale_vaapi}) + return flt diff --git a/enc_hevc_vceenc.py b/enc_hevc_vceenc.py index 07bad76..d8874fa 100644 --- a/enc_hevc_vceenc.py +++ b/enc_hevc_vceenc.py @@ -13,6 +13,7 @@ timeout support: yes smart access: no """ +from lib import BaseEncoder OUTPUT_BUFFER = 32 PROFILES = { @@ -27,9 +28,10 @@ } -class Encoder: +class Encoder(BaseEncoder): CMD = ['vceencc', '--avsw'] + can_scale = True def __init__(self, vid): #print(f'hevc_vceenc idx:{vid.idx()}') @@ -56,5 +58,7 @@ def __init__(self, vid): def get_params(self): return self.params - def scale(self): + def get_filter(self, *args, scale=None, **kwargs): + if not scale: + return {} return ['--output-res', f'-2x{self.res}', '--vpp-resize', 'lanczos3'] diff --git a/enc_hevc_x265.py b/enc_hevc_x265.py index 0862a53..688b131 100644 --- a/enc_hevc_x265.py +++ b/enc_hevc_x265.py @@ -7,6 +7,7 @@ gbrp yuv420p10le yuv422p10le yuv444p10le gbrp10le yuv420p12le yuv422p12le yuv444p12le gbrp12le gray gray10le gray12le """ +from lib import BaseEncoder PRESETS = { 1080: 'medium', @@ -29,7 +30,9 @@ 'yuv422:10': 'main422-10', } -class Encoder: +class Encoder(BaseEncoder): + + can_scale = False def __init__(self, vid): print(f'x265 idx: {vid.idx()}') @@ -49,10 +52,10 @@ def __init__(self, vid): x265params.append(f'{vid.params}') self.params['x265-params'] = ':'.join(x265params) - self.flt = [f'format={FORMATS[vid.idx()]}'] + self.idx = vid.idx() def get_params(self): return self.params - def get_filter(self): - return ','.join(self.flt) + def get_filter(self, *args, scale=None, **kwargs): + return [{'format': FORMATS[self.idx]}] diff --git a/lib.py b/lib.py index 564d2bd..4b3020a 100644 --- a/lib.py +++ b/lib.py @@ -16,6 +16,16 @@ } # FRAME_RATES = (23.98 24 25 29.97 30 50 59.94 60 120 150 180) +class BaseEncoder: + + ERROR = "Method should be overridden in subclasses" + + def get_filter(self, *args, scale=None, **kwargs): + raise NotImplementedError(self.ERROR) + + def get_params(self): + raise NotImplementedError(self.ERROR) + @dataclass class Video: # pylint: disable=too-many-instance-attributes @@ -42,6 +52,20 @@ def gop(frame_rate, gop_mul): return int(float(frame_rate) * gop_mul) return None +def join_filters(filters): + result = [] + for filter_item in filters: + if isinstance(filter_item, str): + result.append(filter_item) + elif isinstance(filter_item, dict): + for key, value in filter_item.items(): + if isinstance(value, dict): + sub_items = [f"{k}={v}" for k, v in value.items()] + result.append(f"{key}=" + ":".join(sub_items)) + else: + result.append(f"{key}={value}") + return ",".join(result) + def format_time(seconds): if seconds < 60: return f"{seconds:.3f}s" diff --git a/mymediainfo.py b/mymediainfo.py index 274a761..462ef4d 100644 --- a/mymediainfo.py +++ b/mymediainfo.py @@ -84,13 +84,20 @@ def print(self): print(f'Bit depth: {self.bit_depth}') print(f'Format: {self.format}') print(f'Format profile: {self.format_profile}') - print(f'Format settings: {self.format_settings}') - print(f'Color format: {self.color_format}') - print(f'Color primaries: {self.color_primaries}') - print(f'Matrix coefficients: {self.matrix_coefficients}') - print(f'Transfer characteristics: {self.transfer_characteristics}') - print(f'Color range: {self.color_range}') + if self.format_settings: + print(f'Format settings: {self.format_settings}') + if self.color_format: + print(f'Color format: {self.color_format}') + if self.color_primaries: + print(f'Color primaries: {self.color_primaries}') + if self.matrix_coefficients: + print(f'Matrix coefficients: {self.matrix_coefficients}') + if self.transfer_characteristics: + print(f'Transfer characteristics: {self.transfer_characteristics}') + if self.color_range: + print(f'Color range: {self.color_range}') print(f'Scan: {self.video_track.scan_type}') - print(f'Audio format: {self.audio_format}') - print(f'Audio sampling rate: {self.audio_sampling_rate}') - print(f'Audio channels: {self.audio_channels}') + if self.audio_track: + print(f'Audio format: {self.audio_format}') + print(f'Audio sampling rate: {self.audio_sampling_rate}') + print(f'Audio channels: {self.audio_channels}')