ported to over-2.0

This commit is contained in:
Martinez 2016-05-18 12:59:13 +02:00
parent de2178c4f9
commit 963a6799da
3 changed files with 97 additions and 141 deletions

View file

@ -12,8 +12,7 @@ import version
from aux import parse_fps, to_Path, update_cfg_context
Command = over.core.cmd.Command
prefix = over.core.text.prefix
Command = over.cmd.Command
# --------------------------------------------------
X264_BANNED_PIXFMTS = {'bgr24', 'yuv422p'}
@ -21,7 +20,7 @@ X264_BANNED_PIXFMTS = {'bgr24', 'yuv422p'}
# --------------------------------------------------
# see doc/command_assembler.png
command = over.core.types.ndict()
command = over.types.ndict()
command.identify = Command('ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', 'INFILE')
command.normalize_prepass = Command('ffmpeg', '-i', 'INFILE', '-filter:a', 'volumedetect', '-f', 'null', '/dev/null')
command.encode_generic = Command('ffmpeg', '-i', 'INFILE', 'MAP', 'VIDEO', 'AUDIO', '-sn', 'OUTFILE')
@ -39,63 +38,64 @@ command.sub_copy_video = Command('-codec:v', 'copy')
# --------------------------------------------------
if __name__ == '__main__':
main = over.core.app.Main('over-video', 'Overwatch Video', version.str, 'AO-JSL', use_cfg_file=True)
main.add_option('audio', 'str', 'vorbis', 'Audio codec to use, either §mvorbis§/, §mpcm§/, §mcopy§/ or §mdrop§/.', short_name='a')
main.add_option('audio-quality', 'float', 4, 'Audio encoding quality with §m-1§/ being the worst and §m10§/ being the best.', short_name='q')
main.add_option('video', 'str', 'x264', 'Video codec to use, either §mx265§/, §mx264§/, §mtheora§/, §mcopy§/ or §mdrop§/.', short_name='v')
main.add_option('video-preset', 'str', 'slow', 'Video encoding preset, if supported by the selected encoder.', short_name='P')
main.add_option('video-quality', 'float', 22, 'Video encoding quality (CRF). Use §m0§/-§m10§/ for Theora (§m0§/ being the lowest, §m5§/-§m7§/ is generally watchable) and §m0§/-§m51§/ for x264/5 (§m0§/ being lossless, §m18§/-§m28§/ is reasonable).', short_name='Q')
main.add_option('context', 'bool', True, 'Use .over-video file in CWD, if available, to remember encoding parameters per-directory.', short_name='C')
main.add_option('normalize', 'bool', True, 'Normalize the audio track.', short_name='n')
main.add_option('normalize-target', 'float', -20.0, 'Target mean volume to target.')
main.add_option('normalize-override', 'float', 0.0, 'Volume correction to use instead of computing the required value in a (lengthy) pre-pass.', short_name='N', use_cfg_file=False)
main.add_option('ffmpeg-vfilter', 'str', '', 'Raw ffmpeg -filter:v options, e.g. "§mscale=1280:trunc(ow/a/2)*2,transpose=dir=1§/"', short_name='F')
main.add_option('ffmpeg-map', 'str', [], 'Raw ffmpeg -map options, e.g. "§m0:1 0:2§/". This is a drop-in fix until we get proper stream selection.', short_name='M', plural=True)
main.add_option('move-source', 'str', 'processed', 'Move source file to this directory after conversion. Use an empty string to disable.', short_name='m')
main.add_option('dump-commands', 'bool', False, 'Print ffmpeg commands that would be executed. If §B--§gnormalize§/ is in effect, the normalization pre-pass will still be performed so that the proper volume correction can be computed.', short_name='D')
main.add_option('probe', 'bool', False, 'Print the raw JSON output of ffprobe and exit.', short_name='p')
main.add_option('armed', 'bool', False, 'Perform the suggested action.', short_name='A', use_cfg_file=False)
main.enable_help('h')
main.add_help('Description', ['Over-Video is a simple video converter.'])
main.add_help('Good encoder settings', ['§Bx264§/: §B--§gvideo§/ §mx264§/ §B--§gvideo-preset§/ §mslow§/ §B--§gvideo-quality§/ §m22§/', '§Bx265§/: §B--§gvideo§/ §mx265§/ §B--§gvideo-preset§/ §mmedium§/ §B--§gvideo-quality§/ §m20§/'])
main.parse()
main = over.app.Main('over-video', version.str, 'AO-JSL', features={'config_file': True})
main.add_option('audio', 'Audio codec to use, either <M>vorbis<.>, <M>pcm<.>, <M>copy<.> or <M>drop<.>.', str, ['vorbis'], abbr='a', count=1)
main.add_option('audio-quality', 'Audio encoding quality with <M>-1<.> being the worst and <M>10<.> being the best.', float, [4], abbr='q', count=1)
main.add_option('video', 'Video codec to use, either <M>x265<.>, <M>x264<.>, <M>theora<.>, <M>copy<.> or <M>drop<.>.', str, ['x264'], abbr='v', count=1)
main.add_option('video-preset', 'Video encoding preset, if supported by the selected encoder.', str, ['slow'], abbr='P', count=1)
main.add_option('video-quality', 'Video encoding quality (CRF). Use <M>0<.>-<M>10<.> for Theora (<M>0<.> being the lowest, <M>5<.>-<M>7<.> is generally watchable) and <M>0<.>-<M>51<.> for x264/5 (<M>0<.> being lossless, <M>18<.>-<M>28<.> is reasonable).', float, [22], abbr='Q', count=1)
main.add_option('context', 'Use .over-video file in CWD, if available, to remember encoding parameters per-directory.', bool, [True], abbr='C')
main.add_option('normalize', 'Normalize the audio track.', bool, [True], abbr='n')
main.add_option('normalize-target', 'Target mean volume to target.', float, [-20.0], count=1)
main.add_option('normalize-override', 'Volume correction to use instead of computing the required value in a (lengthy) pre-pass.', float, [0.0], abbr='N', count=1, in_cfg_file=False)
main.add_option('ffmpeg-vfilter', 'Raw ffmpeg -filter:v options, e.g. "<M>scale=1280:trunc(ow/a/2)*2,transpose=dir=1<.>"', str, [''], abbr='F', count=1)
main.add_option('ffmpeg-map', 'Raw ffmpeg <c>-map<.> options, e.g. <W>--<g>map<.> <M>0:1<.> <W>--<g>map<.> <M>0:2<.>. This is a drop-in fix until we get proper stream selection.', str, abbr='M', overwrite=False, count=1)
main.add_option('move-source', 'Move source file to this directory after conversion. Use an empty string to disable.', str, ['processed'], count=1)
main.add_option('dump-commands', 'Print ffmpeg commands that would be executed. If <W>--<g>normalize<.> is in effect, the normalization pre-pass will still be performed so that the proper volume correction can be computed.', bool, [False], abbr='D', in_cfg_file=False)
main.add_option('probe', 'Print the raw JSON output of ffprobe and exit.', bool, [False], abbr='p', in_cfg_file=False)
main.add_option('armed', 'Perform the suggested action.', bool, [False], abbr='A', in_cfg_file=False)
main.add_doc('Description', ['Over-Video is a simple video converter.'])
main.add_doc('Good encoder settings', ['<W>x264<.>: <W>--<g>video<.> <M>x264<.> <W>--<g>video-preset<.> <M>slow<.> <W>--<g>video-quality<.> <M>22<.>', '<W>x265<.>: <W>--<g>video<.> <M>x265<.> <W>--<g>video-preset<.> <M>medium<.> <W>--<g>video-quality<.> <M>20<.>'])
main.setup()
# --------------------------------------------------
# cfg checks
files = over.core.types.ndict()
files = over.types.ndict()
audio_words = []
video_words = []
files.container = 'mkv'
if main.cfg.context:
update_cfg_context(main, ["armed", "probe", "dump-commands", "ffmpeg-map", "normalize-override"])
update_cfg_context(main, ["context", "armed", "probe", "dump-commands", "ffmpeg-map", "normalize-override"])
if main.cfg.audio in ('copy', 'drop'):
audio_words.append('§c%s§/' %(main.cfg.audio))
audio_words.append('<c>%s<.>' %(main.cfg.audio))
else:
audio_words.append('§gcodec§/=§m%s§/' %(main.cfg.audio))
audio_words.append('<g>codec<.>=<M>%s<.>' %(main.cfg.audio))
if main.cfg.audio == 'vorbis':
audio_words.append('§gquality§/=§m%.1f§/' %(main.cfg.audio_quality))
audio_words.append('<g>quality<.>=<M>%.1f<.>' %(main.cfg.audio_quality))
if main.cfg.normalize_override != 0:
audio_words.append('§gadjust_volume§/=§m%.1f dB§/' %(main.cfg.normalize_override))
audio_words.append('<g>adjust_volume<.>=<M>%.1f dB<.>' %(main.cfg.normalize_override))
elif main.cfg.normalize:
audio_words.append('§gnormalize§/=§m%.1f dB§/' %(main.cfg.normalize_target))
audio_words.append('<g>normalize<.>=<M>%.1f dB<.>' %(main.cfg.normalize_target))
if main.cfg.video in ('copy', 'drop'):
video_words.append('§c%s§/' %(main.cfg.video))
video_words.append('<c>%s<.>' %(main.cfg.video))
else:
video_words.append('§gcodec§/=§m%s§/' %(main.cfg.video))
video_words.append('§gquality§/=§m%.1f§/' %(main.cfg.video_quality))
video_words.append('<g>codec<.>=<M>%s<.>' %(main.cfg.video))
video_words.append('<g>quality<.>=<M>%.1f<.>' %(main.cfg.video_quality))
if main.cfg.video_preset and main.cfg.video in ('x264', 'x265'):
video_words.append('§gpreset§/=§m%s§/' %(main.cfg.video_preset))
video_words.append('<g>preset<.>=<M>%s<.>' %(main.cfg.video_preset))
if main.cfg.ffmpeg_vfilter:
video_words.append('§gvfilter§/=§m%s§/' %(main.cfg.ffmpeg_vfilter))
video_words.append('<g>vfilter<.>=<M>%s<.>' %(main.cfg.ffmpeg_vfilter))
if main.cfg.video == 'drop':
if main.cfg.audio == 'pcm':
@ -103,24 +103,22 @@ if __name__ == '__main__':
elif main.cfg.audio == 'vorbis':
files.container = 'ogg'
main.print('settings', prefix.start, suffix=':\n')
main.print('settings', main.print.tl.start, end=':\n')
main.print('audio: %s' %(', '.join(audio_words)))
main.print('video: %s' %(', '.join(video_words)))
main.print('container: §gtype§/=§m%s§/' %(files.container))
main.print('container: <g>type<.>=<M>%s<.>' %(files.container))
if main.cfg.move_source:
main.print('move source files to §B%s§//' %(main.cfg.move_source))
main.print('move source files to <W>%s<.>/' %(main.cfg.move_source))
if main.cfg.audio not in ('drop', 'copy', 'pcm', 'vorbis'):
main.print('unknown audio codec', prefix.fail)
raise RuntimeError
raise ValueError('unknown audio codec: %s' %(main.cfg.audio))
if main.cfg.video not in ('drop', 'copy', 'theora', 'x264', 'x265'):
main.print('unknown video codec', prefix.fail)
raise RuntimeError
raise ValueError('unknown video codec: %s' %(main.cfg.video))
if not main.targets:
main.print('no files specified', prefix.warn)
main.print('no files specified', main.print.tl.warn)
for tgt in main.targets:
print()
@ -131,11 +129,11 @@ if __name__ == '__main__':
files.move_infile_to = to_Path(main.cfg.move_source) / files.infile.name if main.cfg.move_source else None
if not os.path.exists(tgt) or os.path.isdir(tgt):
main.print('target §y%s§/ §ris not a readable file§/, skipping' %(tgt), prefix.fail)
main.print('target <y>%s<.> <r>is not a readable file<.>, skipping' %(tgt), main.print.tl.fail)
continue
original_filesize = over.core.text.Unit(files.infile.stat().st_size, 'o')
main.print('processing §B%s§/ (%s)' %(tgt, original_filesize), prefix.start)
original_filesize = over.text.Unit(files.infile.stat().st_size, 'o')
main.print('processing <W>%s<.> (%s)' %(tgt, original_filesize), main.print.tl.start)
# --------------------------------------------------
# identify the input file
@ -150,7 +148,7 @@ if __name__ == '__main__':
print(identify_raw)
continue
info = over.core.types.ndict()
info = over.types.ndict()
try:
info.duration = float(identify_dict['format']['duration'])
@ -162,10 +160,10 @@ if __name__ == '__main__':
amount_as = len(audio_streams)
if amount_vs > 1:
main.print('detected §r%d§/ video streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_vs), prefix.warn)
main.print('detected <r>%d<.> video streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_vs), main.print.tl.warn)
if amount_as > 1:
main.print('detected §y%d§/ audio streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_as), prefix.warn)
main.print('detected <y>%d<.> audio streams, picking the "best" one (see man 1 ffmpeg, section STREAM SELECTION)' %(amount_as), main.print.tl.warn)
if video_streams:
# ffmpeg picks the stream with the highest pixel count and then the lowest index
@ -174,14 +172,14 @@ if __name__ == '__main__':
info.video_codec = video['codec_name']
info.video_size_x = video['width']
info.video_size_y = video['height']
info.video_fps = over.core.text.Unit(parse_fps(video['r_frame_rate']), 'Hz')
info.video_fps = over.text.Unit(parse_fps(video['r_frame_rate']), 'Hz')
if 'bit_rate' in video:
info.video_bitrate = over.core.text.Unit(video['bit_rate'], 'b/s')
info.video_bitrate = over.text.Unit(video['bit_rate'], 'b/s')
elif 'tags' in video and 'BPS' in video['tags']:
info.video_bitrate = over.core.text.Unit(int(video['tags']['BPS']), 'b/s')
info.video_bitrate = over.text.Unit(int(video['tags']['BPS']), 'b/s')
else:
info.video_bitrate = '§r??§/'
info.video_bitrate = '<R>??<.>'
info.pixel_fmt = video['pix_fmt']
else:
info.video_fps = 30 # faked for progress bars
@ -192,24 +190,24 @@ if __name__ == '__main__':
audio = audio_streams[0]
info.audio_codec = audio['codec_name']
info.audio_channels = audio['channels']
info.audio_samplerate = over.core.text.Unit(audio['sample_rate'], 'Hz')
info.audio_samplerate = over.text.Unit(audio['sample_rate'], 'Hz')
info.audio_language = audio['tags']['language'] if 'tags' in audio and 'language' in audio['tags'] else 'und'
info.audio_bitrate = over.core.text.Unit(audio['bit_rate'], 'b/s') if 'bit_rate' in audio else '§r??§/'
info.audio_bitrate = over.text.Unit(audio['bit_rate'], 'b/s') if 'bit_rate' in audio else '<R>??<.>'
except:
main.print('exception while reading identify_dict, dump follows', prefix.fail)
main.print('exception while reading identify_dict, dump follows', main.print.tl.fail)
print(identify_dict)
raise
if video_streams:
main.print('§mvideo§/: size=§b%d§/x§b%d§/ px, framerate=%s, codec=%s, bitrate=%s' %(info.video_size_x, info.video_size_y, info.video_fps, info.video_codec, info.video_bitrate))
main.print('<m>video<.>: size=<M>%d<.>x<M>%d<.> px, framerate=%s, codec=%s, bitrate=%s' %(info.video_size_x, info.video_size_y, info.video_fps, info.video_codec, info.video_bitrate))
else:
main.print('§mvideo§/: §yNone§/', prefix.warn)
main.print('<m>video<.>: <y>None<.>', main.print.tl.warn)
if audio_streams:
main.print('§caudio§/: channels=§b%d§/, samplerate=%s, codec=%s, bitrate=%s, language=%s' %(info.audio_channels, info.audio_samplerate, info.audio_codec, info.audio_bitrate, info.audio_language))
main.print('<c>audio<.>: channels=<C>%d<.>, samplerate=%s, codec=%s, bitrate=%s, language=%s' %(info.audio_channels, info.audio_samplerate, info.audio_codec, info.audio_bitrate, info.audio_language))
else:
main.print('§caudio§/: §yNone§/', prefix.warn)
main.print('<c>audio<.>: <y>None<.>', main.print.tl.warn)
# --------------------------------------------------
# normalization pre-pass
@ -222,7 +220,7 @@ if __name__ == '__main__':
command.normalize_prepass.INFILE = 'file:' + str(files.infile)
command.normalize_prepass.run(stderr=True)
pb = over.core.text.ProgressBar2(
pb = over.text.ProgressBar(
"§%a [§=a>§ A] §sa (Trem=§TA)",
{
"a": {
@ -264,7 +262,7 @@ if __name__ == '__main__':
info.max_correction = -float(re.findall(b'max_volume: (-?\d+\.\d+) dB', output)[0])
info.volume_correction = main.cfg.normalize_target - info.mean_volume
else:
main.print('§runexpected ffmpeg output§/, dump follows', prefix.fail, suffix=':\n')
main.print('<r>unexpected ffmpeg output<.>, dump follows', main.print.tl.fail, suffix=':\n')
print(output.decode('utf-8'))
raise RuntimeError
@ -272,11 +270,11 @@ if __name__ == '__main__':
if info.volume_correction > info.max_correction:
d = info.volume_correction - info.max_correction
main.print("suggested correction is %.1f dB above the stream's maximum and will cause clipping" %(d), prefix.warn)
main.print("suggested correction is %.1f dB above the stream's maximum and will cause clipping" %(d), main.print.tl.warn)
else:
info.volume_correction = main.cfg.normalize_override
main.print('using user-supplied volume correction §m%.1f dB§/' %(info.volume_correction))
main.print('using user-supplied volume correction <M>%.1f dB<.>' %(info.volume_correction))
info.normalize_command = command.sub_normalize
info.normalize_command.reset()
@ -345,7 +343,7 @@ if __name__ == '__main__':
command.sub_x264.VFILTER = info.vfilter_command
if info.pixel_fmt in X264_BANNED_PIXFMTS:
main.print('source pixel format §r%s§/ is incompatible with x264, forcing §yyuv420p§/' %(info.pixel_fmt), prefix.warn)
main.print('source pixel format <r>%s<.> is incompatible with x264, forcing <y>yuv420p<.>' %(info.pixel_fmt), main.print.tl.warn)
command.sub_x264.PIXFMT = command.force_yuv420p
else:
command.sub_x264.PIXFMT = None
@ -367,14 +365,14 @@ if __name__ == '__main__':
cmd = ' '.join(encode_cmd.dump(pretty=True))
if main.cfg.armed:
main.print('executing §B%s§/' %(cmd), prefix.start)
main.print('executing <W>%s<.>' %(cmd), main.print.tl.start)
else:
main.print('will execute §B%s§/' %(cmd))
main.print('will execute <W>%s<.>' %(cmd))
else:
main.print('will encode into §B%s§/' %(files.tmpfile))
main.print('will encode into <W>%s<.>' %(files.tmpfile))
if main.cfg.armed:
pb = over.core.text.ProgressBar2(
pb = over.text.ProgressBar(
"§%f §rs [§=f>§ F] §sf (§ss) (Sest=§zs, Trem=§TF)",
{
"f": {
@ -415,14 +413,14 @@ if __name__ == '__main__':
pb.render()
new_filesize = over.core.text.Unit(files.tmpfile.stat().st_size, 'o')
new_filesize = over.text.Unit(files.tmpfile.stat().st_size, 'o')
pb.end()
if encode_cmd.returncode == 0:
main.print('encoding finished: %s -> %s' %(original_filesize, new_filesize), prefix.done)
main.print('encoding finished: %s -> %s' %(original_filesize, new_filesize), main.print.tl.done)
else:
main.print('§rencoding failed§/, ffmpeg returned §y%d§/' %(encode_cmd.returncode), prefix.fail)
main.print('<r>encoding failed<.>, ffmpeg returned <y>%d<.>' %(encode_cmd.returncode), main.print.tl.fail)
raise RuntimeError
# --------------------------------------------------
@ -433,20 +431,20 @@ if __name__ == '__main__':
if not move_to_dir.is_dir():
if main.cfg.armed:
main.print('creating directory §B%s§/' %(move_to_dir), prefix.start)
main.print('creating directory <W>%s<.>' %(move_to_dir), main.print.tl.start)
move_to_dir.mkdir()
else:
main.print('will create directory §B%s§/' %(move_to_dir))
main.print('will create directory <W>%s<.>' %(move_to_dir))
if files.move_infile_to:
if main.cfg.armed:
main.print('moving §B%s§/ -> §B%s§/' %(files.infile, files.move_infile_to), prefix.start)
main.print('moving <W>%s<.> -> <W>%s<.>' %(files.infile, files.move_infile_to), main.print.tl.start)
files.infile.rename(files.move_infile_to)
else:
main.print('will move §B%s§/ -> §B%s§/' %(files.infile, files.move_infile_to))
main.print('will move <W>%s<.> -> <W>%s<.>' %(files.infile, files.move_infile_to))
if main.cfg.armed:
main.print('moving §B%s§/ -> §B%s§/' %(files.tmpfile, files.outfile), prefix.start)
main.print('moving <W>%s<.> -> <W>%s<.>' %(files.tmpfile, files.outfile), main.print.tl.start)
files.tmpfile.rename(files.outfile)
else:
main.print('will move §B%s§/ -> §B%s§/' %(files.tmpfile, files.outfile))
main.print('will move <W>%s<.> -> <W>%s<.>' %(files.tmpfile, files.outfile))