Skip to content

Commit

Permalink
better py; nvenc fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
porzione committed Jul 7, 2024
1 parent 7cbbbc5 commit bc4b80c
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 84 deletions.
173 changes: 99 additions & 74 deletions cpmyvideos.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
import argparse
from timeit import default_timer as timer
import os
import sys
import shutil
#from pprint import pprint
import magic
import dateparser
from mymediainfo import MyMediaInfo
from lib import format_time, run_cmd, ENCODERS

RESOLUTIONS = (720, 1080, 1440, 2160) # 1620 1800
DEF_RES = 1440
FF_PRESETS = ('ultrafast', 'superfast', 'veryfast', 'faster', 'fast',
'medium', 'slow', 'slower', 'veryslow')
RESOLUTIONS = ('720', '1080', '1440', '1620', '2160')
DEF_RES = '1440'
HEVC_DEF_PRESET = 'medium'
SVTAV1_PRESETS = (0,13) # range
SVTAV1_DEF_PRESET = '6'
DNXHR = ('lb', 'sq', 'hq', 'hqx', '444')
NVENC_DEF_PRESET='p5' # p7=2pass
NVENC_DEF_TUNE='hq'
FORMATS = ('dnxhr', 'hevc', 'av1')
FORMAT_EXTENSIONS = {
'av1': 'MP4',
Expand All @@ -25,8 +30,9 @@
}
BIT_DEPTHS = {'8', '10'}
CRF = {
'hevc': '18',
'av1': '28',
'hevc': '18', # 20
'av1': '28', # 30, default 35
'nv': '19',
'dnxhr': None,
}

Expand All @@ -43,24 +49,25 @@
#parser.add_argument('-y', action='store_true', help='Overwrite')
parser.add_argument('--dns', action='store_true',
help=f'Do Not Scale to {DEF_RES}')
parser.add_argument('--res', default=DEF_RES, type=int,
parser.add_argument('--res', default=DEF_RES, choices=RESOLUTIONS,
help='Resolution (%(default)s)')
parser.add_argument('--fmt', default='hevc',
parser.add_argument('--fmt', default='hevc', choices=FORMATS,
help='Target format (%(default)s)')
parser.add_argument('--preset', default='slow',
help='ffmpeg preset (%(default)s)')
parser.add_argument('--svtav1preset', default='4',
help='svtav1 preset (%(default)s)')
parser.add_argument('--crf',
help=f'ffmpeg crf ({CRF})')
parser.add_argument('--enc', default='sw',
help='Encoder type (%(default)s)')
parser.add_argument('--bits', help='Target bit depth 8|10')
parser.add_argument('--preset', help='Preset HEVC/SVT-AV1/NVENC')
parser.add_argument('--tune', help='Tune HEVC/SVT-AV1/NVENC')
parser.add_argument('--crf', help=f'CRF/quality ({CRF})')
parser.add_argument('--params', help='Param HEVC/SVT-AV1')
parser.add_argument('--dnx', choices=DNXHR, help='DNxHR profile')
parser.add_argument('--enc', default='sw', choices=ENCODERS,
help='Encoder (%(default)s)')
parser.add_argument('--bits', choices=BIT_DEPTHS, help='Force bit depth')
parser.add_argument('--nometa', action='store_true',
help='Do not map metadata')
parser.add_argument('--fnparams', action='store_true',
help='Add params to dest file names')
parser.add_argument('-t', dest='duration')
parser.add_argument('-1', dest='first', action='store_true',
help='only 1st found file')
parser.add_argument('--dry', action='store_true',
help='Dry run')
args = parser.parse_args()
Expand All @@ -73,22 +80,25 @@
raise ValueError(f"Source dir '{args.srcdir}' doesn't exist")
if not os.path.exists(args.dstdir):
raise ValueError(f"Destination dir '{args.dstdir}' doesn't exist")
if not args.res in RESOLUTIONS:
raise ValueError(f"Bad resolution '{args.res}', use one of: "
f"{', '.join(map(str,RESOLUTIONS))}")
if not args.fmt in FORMATS:
raise ValueError(f"Bad format '{args.fmt}', use one of: {', '.join(FORMATS)}")
if not args.preset in FF_PRESETS:
raise ValueError(f"Bad preset '{args.preset}', use one of:"
f"{', '.join(FF_PRESETS)}")
if not args.enc in ENCODERS:
raise ValueError(f"Bad encoder '{args.enc}', use one of: {', '.join(ENCODERS)}")
if args.bits and args.bits not in BIT_DEPTHS:
raise ValueError(f"Bad bit depth '{args.bits}', use one of: "
f"{', '.join(BIT_DEPTHS)}")
#print(args) ; exit()

crf = args.crf if args.crf else CRF[args.fmt]
crf = args.crf if args.crf else CRF.get(args.enc) or CRF[args.fmt]
if crf:
print(f'CRF: {crf}')

preset = None
if args.preset:
preset = args.preset
else:
if args.enc == 'nv':
preset = NVENC_DEF_PRESET
elif args.enc == 'sw':
if args.fmt == 'hevc':
preset = HEVC_DEF_PRESET
elif args.fmt == 'av1':
preset = SVTAV1_DEF_PRESET
if preset:
print(f'Preset: {preset}')

if args.newer:
args.newer = dateparser.parse(args.newer).timestamp()
Expand All @@ -97,37 +107,32 @@ def v_transcode(src, dst, info):
cmd = ['ffmpeg', '-hide_banner', '-nostdin', '-ignore_editlist', '1']
params_in = {}
filter_v = {}
bit_depth = args.bits or str(info.bit_depth())
if info.is_hq():
params = { 'c:v': 'copy' }
elif args.fmt == 'dnxhr':
# https://dovidenko.com/2019/999/ffmpeg-dnxhd-dnxhr-mxf-proxies-and-optimized-media.html
# dnxhr_lb - Low Bandwidth. 8-bit 4:2:2 (yuv422p). Offline Quality.
# dnxhr_sq - Standard Quality. 8-bit 4:2:2 (yuv422p). Suitable for delivery format.
# dnxhr_hq - High Quality. 8-bit 4:2:2 (yuv422p).
# dnxhr_hqx - High Quality. 10-bit 4:2:2 (yuv422p10le). UHD/4K Broadcast-quality delivery.
# dnxhr_444 - Finishing Quality. 10-bit 4:4:4 (yuv444p10le). Cinema-quality delivery.
# dnxhr_lb Low Bandwidth. 8-bit 4:2:2 (yuv422p). Offline Quality. 22:1
# dnxhr_sq Standard Quality. 8-bit 4:2:2 (yuv422p). Suitable for delivery. 7:1
# dnxhr_hq High Quality. 8-bit 4:2:2 (yuv422p). 4.5:1
# dnxhr_hqx High Quality. 10-bit 4:2:2 (yuv422p10le). UHD/4K Broadcast-quality. 5.5:1
# dnxhr_444 Finishing Quality. 10-bit 4:4:4 (yuv444p10le). Cinema-quality. 4.5:1
bit_depth = args.bits or str(info.bit_depth())
params = {'c:v': 'dnxhd'}
if bit_depth == '8':
filter_v['format'] = 'yuv422p'
params['profile:v'] = 'dnxhr_hq'
elif bit_depth == '10':
filter_v['format'] = 'yuv420p10le'
params['profile:v'] = 'dnxhr_hqx'
filter_v['format'] = 'yuv422p' if bit_depth == '8' else 'yuv422p10le'
default_profile = 'dnxhr_hq' if bit_depth == '8' else 'dnxhr_hqx'
params['profile:v'] = f'dnxhr_{args.dnx}' if args.dnx else default_profile

elif args.fmt == 'hevc':
# http://trac.ffmpeg.org/wiki/Encode/H.265
# https://trac.ffmpeg.org/wiki/Hardware/AMF
# https://github.com/GPUOpen-LibrariesAndSDKs/AMF/wiki/FFmpeg-and-AMF-HW-Acceleration
# Panasonic 420/8 420/10 422/10
bit_depth = args.bits or '10'
match args.enc:
# https://trac.ffmpeg.org/wiki/Hardware/AMF
# https://github.com/GPUOpen-LibrariesAndSDKs/AMF/wiki/FFmpeg-and-AMF-HW-Acceleration
case 'amf':
params = {
'c:v': 'hevc_amf',
'usage': 'lowlatency_high_quality',
# 'profile:v': 'main' # only main
'profile_tier': 'high',
'quality': 'quality',
'rc': 'cqp',
'rc:v': 'cqp',
'qp_p': crf,
'qp_i': crf,
}
Expand All @@ -147,42 +152,46 @@ def v_transcode(src, dst, info):
'rc_mode': 'CQP',
'compression_level': '29',
'qp': crf,
# 'async_depth': '4'
'tier': 'high',
}
case 'nv':
# https://docs.nvidia.com/video-technologies/video-codec-sdk/12.0/ffmpeg-with-nvidia-gpu/index.html
# https://developer.nvidia.com/blog/nvidia-ffmpeg-transcoding-guide/
params_in = {
'hwaccel': 'cuda',
# keeps the decoded frames in GPU memory
'hwaccel_output_format': 'cuda'
}
params = {
'c:v': 'hevc_nvenc',
'fps_mode': 'passthrough',
'preset': 'p5', # p6,p7
'preset:v': preset,
'tune:v': args.tune or NVENC_DEF_TUNE,
'rc:v': 'vbr',
'cq:v': crf,
'b:v': '0',
'tier': 'high',
'tune': 'hq',
# new
'rc': 'constqp',
'qp': crf
#'profile:v': 'high',
}
if bit_depth == '10':
params['profile:v'] = 'main10'
case 'sw':
# https://x265.readthedocs.io/en/stable/
params = {
'c:v': 'libx265',
'preset': args.preset,
'preset': preset,
'crf': crf, # default 28
'x265-params': 'level-idc=5.1'
}
if bit_depth == '10':
params['profile:v'] = 'main10'
#params['x265-params'] += ':profile=main10'
filter_v['format'] = 'yuv420p10le'

# params['tag:v'] = 'hvc1' # apple/qt
if args.tune:
params['x265-params'] += f':tune={args.tune}'
if args.params:
params['x265-params'] += f':{args.params}'
elif args.fmt == 'av1':
# https://trac.ffmpeg.org/wiki/Encode/AV1
bit_depth = args.bits or '10'
match args.enc:
case 'amf':
params = {
Expand All @@ -200,7 +209,6 @@ def v_transcode(src, dst, info):
params = {
'c:v': 'av1_vaapi',
'compression_level': '29',
'tier': 'high',
}
case 'nv':
params_in = {
Expand All @@ -210,34 +218,36 @@ def v_transcode(src, dst, info):
params = {
'c:v': 'av1_nvenc',
'fps_mode': 'passthrough',
#'preset': 'slow',
#'preset': '6',
'preset': preset,
'tune': 'hq',
}
case 'sw':
params = {
'c:v': 'libsvtav1',
'preset': args.svtav1preset,
'crf': crf, # default 35
# 'qp': '35'
'svtav1-params': 'rc=0:level=5.2',
'preset': preset,
'crf': crf,
'svtav1-params': 'rc=0:film-grain-denoise=0:enable-overlays=1',
}
if bit_depth == '8':
filter_v['format'] = 'yuv422p'
elif bit_depth == '10':
filter_v['format'] = 'yuv420p10le'
# input-depth 8|10
params['svtav1-params'] += 'input-depth=10'
params['svtav1-params'] += ':input-depth=10'
if args.tune:
params['svtav1-params'] += f':tune={args.tune}'
if args.params:
params['svtav1-params'] += f':{args.params}'

if info.height() > args.res and not args.dns:
if info.height() > int(args.res) and not args.dns:
match args.enc:
case 'nv':
# scale_cuda | scale_npp (`--enable-cuda-nvcc`)
filter_v['scale_cuda'] = f'w=-1:h={args.res}:interp_algo=lanczos'
if bit_depth == '10':
filter_v['scale_cuda'] += ':format=p010le'
case 'vaapi':
# mode=hq|nl_anamorphic
filter_v['scale_vaapi'] = f'w=-1:h={args.res}:mode=hq'
filter_v['scale_vaapi'] = f'w=-1:h={args.res}:mode=hq:force_original_aspect_ratio=1'
if bit_depth == '10':
filter_v['scale_vaapi'] += ':format=p010'
case _:
Expand Down Expand Up @@ -285,7 +295,7 @@ def v_transcode(src, dst, info):
cmd.extend([f'-{key}', value])

cmd.append(dst)
run_cmd(cmd, args.dry)
return run_cmd(cmd, args.dry)

def v_copy(src, dst):
print(f"COPY {src} {dst}")
Expand All @@ -306,6 +316,14 @@ def v_copy(src, dst):
base_name += f'_crf{crf}'
if args.bits:
base_name += f'_bits{args.bits}'
if args.res and not args.dns:
base_name += f'_res{args.res}'
if args.preset:
base_name += f'_pr{args.preset}'
if args.tune:
base_name += f'_tun{args.tune}'
if args.dnx:
base_name += f'_dnx{args.dnx}'
dst_file = os.path.join(args.dstdir, f'{base_name}.{ext}')
if os.path.exists(dst_file):
print(f'EXISTS {dst_file}')
Expand All @@ -327,10 +345,17 @@ def v_copy(src, dst):
if args.copy:
v_copy(src_file, dst_file)
else:
v_transcode(src_file, dst_file, mi)
rc = v_transcode(src_file, dst_file, mi)
if rc != 0:
print(f'v_transcode failed with code: {rc}')
if os.path.exists(dst_file) and os.path.getsize(dst_file) == 0:
os.remove(dst_file)
sys.exit(1)

end_time = timer() - start_time
print(f"TIME {format_time(end_time)}\n")
TOTAL_TIME += end_time
if args.first:
break

print(f"TOTAL TIME: {format_time(TOTAL_TIME)}")
7 changes: 4 additions & 3 deletions lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def format_time(seconds):
def run_cmd(cmd, dry=False):
print(f"COMMAND: {' '.join(cmd)}")
if dry:
return
return 0

def reader_thread(pipe, process):
for line in iter(pipe.readline, b''):
Expand All @@ -42,5 +42,6 @@ def reader_thread(pipe, process):
stdout_thread.join()
stderr_thread.join()

if return_code != 0:
print(f"Failed with return code: {return_code}")
#if return_code != 0:
# print(f"Failed with return code: {return_code}")
return return_code
15 changes: 8 additions & 7 deletions mymediainfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ def height(self):
def frame_rate(self):
return self.video_track.frame_rate if self.video_track else None

def is_interlaced(self):
return self.video_track.scan_type != 'Progressive'

def is_hq(self):
return self.format() in ['VC-3', 'FFV1', 'ProRes', 'HFYU']

def video_data(self):
return self.video_track.to_data()

Expand All @@ -62,20 +68,15 @@ def audio_sampling_rate(self):
def audio_data(self):
return self.audio_track.to_data()

def is_hq(self):
if self.format() in ['VC-3', 'FFV1', 'ProRes', 'HFYU']:
return True
return False

def print(self):
print(f'Video: {self.width()}x{self.height()} @ {self.frame_rate()}')
print(f"Bit rate: {self.bit_rate()}")
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 space: {self.color_space()}')
print(f'Chroma subsampling: {self.video_track.chroma_subsampling}')
print(f'Scan: {self.video_track.scan_type}')
print(f'Color: {self.color_space()} {self.video_track.chroma_subsampling}')
print(f"Audio format: {self.audio_format()}")
print(f"Audio sampling rate: {self.audio_sampling_rate()}")
print(f"Audio channels: {self.audio_channels()}")
1 change: 1 addition & 0 deletions youtube.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def run(self):
params['qp_p'] = self.args.crf
params['qp_b'] = self.args.crf
case 'vaapi':
# ffmpeg -hide_banner -h encoder=h264_vaapi|less
params_in = {
'threads': '1',
'hwaccel': 'vaapi',
Expand Down

0 comments on commit bc4b80c

Please sign in to comment.