diff --git a/README.md b/README.md index c6035c0..1fccaca 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,15 @@ Hardware support is off by default. Supports Windows, Linux, macOS and probably other OSes. -## Roku mode +## About Roku mode -Made for fast recoding videos for Roku (and probably other smart TVs). -Encode to .mkv using NVENC HEVC. Subtitles will be saved as embedded SRT. Audio will be transcoded to Stereo 96K Opus. -This will allow sound track and subtitle selection, and reduce file size. +Made for fast reformatting videos for compatibility with [Roku](https://www.roku.com/) (and probably other smart TVs). + +* Container is mkv +* Subtitles saved as embedded SRT +* Audio is downmixed to stereo, normalized and transcoded to Opus 96k + +This will allow soundtrack and subtitle selection, and reduces problems with sound quality. ## About hardware encoding @@ -64,7 +68,7 @@ extract 3 exe files from [archive](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git Example with nVidia hardware encoding: ```cmd -./filmcompress.exe --encoder nvidia "c:\\Users\\username\\Pictures\\My Vacation" -o "c:\\Users\\username\\Pictures\\My Vacation\\compressed" +./filmcompress.exe --gpu nvidia "c:\\Users\\username\\Pictures\\My Vacation" "c:\\Users\\username\\Pictures\\My Vacation\\compressed" ``` Remember, you need double slashes in Windows. @@ -75,6 +79,7 @@ Remember, you need double slashes in Windows. ## See also +* [Encoding UHD 4K HDR10 videos with FFmpeg](https://codecalamity.com/encoding-uhd-4k-hdr10-videos-with-ffmpeg/) * [Fastflix](https://github.com/cdgriffith/FastFlix) - Cross platform transcoding GUI * [Videomass](https://pypi.org/project/videomass/) - Cross platform transcoding GUI * [Unmanic](https://github.com/Josh5/unmanic) - Linux mass trancoder @@ -83,5 +88,4 @@ Remember, you need double slashes in Windows. * [Av1an](https://github.com/master-of-zen/Av1an) * [NVEnv](https://github.com/rigaya/NVEnc) * [media-autobuild_suite](https://github.com/m-ab-s/media-autobuild_suite) -* [webm.py](https://github.com/Kagami/webm.py) * List of AV1 tools [here](https://nwgat.ninja/test-driving-aomedias-av1-codec/) \ No newline at end of file diff --git a/build_ffmpeg.sh b/build_ffmpeg.sh index 8830cc0..13fe655 100644 --- a/build_ffmpeg.sh +++ b/build_ffmpeg.sh @@ -14,8 +14,8 @@ set -ex export DEBIAN_FRONTEND=noninteractive -export RUSTFLAGS="-C target-feature=+avx2,+fma" -export CFLAGS="-march=native -mavx2 -mfma -ftree-vectorize -pipe" +export RUSTFLAGS="-C target-feature=+avx2,+fma" +export CFLAGS="-march=native -mavx2 -mfma -ftree-vectorize -pipe" sudo apt-get -y install libass-dev libfreetype6-dev libgnutls28-dev libsdl2-dev libtool python3-pip nasm cmake sudo apt-get -y install libvdpau-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libunistring-dev diff --git a/filmcompress.py b/filmcompress.py index 1c82f0e..f9e7e69 100644 --- a/filmcompress.py +++ b/filmcompress.py @@ -3,7 +3,7 @@ import fnmatch import os import pathlib -import subprocess +from subprocess import run import sys from typing import Iterable import click @@ -11,14 +11,16 @@ # pip install ffmpeg-python import ffmpeg -__version__ = '0.4.3' +__version__ = '0.4.4' SUPPORTED_FORMATS = ['mp4', 'mov', 'm4a', 'mkv', 'webm', 'avi', '3gp'] -SKIPPED_FORMATS = ['hevc', 'av1'] +SKIPPED_CODECS = ['hevc', 'av1'] # Ported from: https://github.com/victordomingos/optimize-images def search_files(dirpath: str, recursive: bool) -> Iterable[str]: - if recursive: + if os.path.isfile(dirpath): + yield os.path.normpath(dirpath) + elif recursive: for root, dirs, files in os.walk(dirpath): for f in files: if not os.path.isfile(os.path.join(root, f)): @@ -38,28 +40,28 @@ def search_files(dirpath: str, recursive: bool) -> Iterable[str]: @click.command() @click.argument('indir', type=click.Path()) -@click.option('-o', '--outdir', type=click.Path(writable=True)) -@click.option('--roku', is_flag=True, help="Defaults for Roku player") +@click.argument('outdir', type=click.Path(exists=True, writable=True), required=False) +@click.option('--roku', is_flag=True, help="Prepare file for Roku player") @click.option('-f', '--oformat', help="Output file format, mp4 is default", default='mp4') @click.option('-r', '--recursive', is_flag=True, help='Recursive') -@click.option('--av1', help='AV1 codec (experimental)', type=click.Choice(['aom', 'svt', 'rav1e'], case_sensitive=False), default='aom') -@click.option('-g', '--gpu', type=click.Choice(['nvidia', 'intel', 'amd'], case_sensitive=False), help='Use GPU of type. Can be: nvidia, intel, amd. Defaults to none (recommended).') -@click.option('--include', default='*') -@click.option('-i', '--info', is_flag=True, help='Only enumerate codecs. Do not transcode.') -def main(indir, outdir, oformat='mp4', include='*', recursive=False, gpu='none', av1='aom', info=False, roku=False): - """ Compress h264 video files in a directory using libx265 codec with crf=28 +@click.option('--av1', help='AV1 codec (experimental)', type=click.Choice(['aom', 'svt', 'rav1e'], case_sensitive=False)) +@click.option('-g', '--gpu', type=click.Choice(['nvidia', 'intel', 'amd', 'none'], case_sensitive=False), help='Use GPU of type. Can be: nvidia, intel, amd. Defaults to none (recommended).') +@click.option('-i', '--include', default='*') +@click.option('-n', '--notranscode', is_flag=True, help='Skip any transcoding, good with Roku mode') +def main(indir, av1, outdir=None, oformat='mp4', include='*', recursive=False, gpu='none', roku=False, notranscode=False): + """ Compress h264 video files in a directory using libx265 codec indir: the directory to scan for video files outdir: output directory recursive: whether to search directory or all its contents gpu: type of hardware encoder av1: use experimetal av1 encoder - info: only list codecs """ outdir = pathlib.PurePath(outdir) total = 0 + command_line = '' if recursive: print('Processing recursively starting from', indir) @@ -68,6 +70,9 @@ def main(indir, outdir, oformat='mp4', include='*', recursive=False, gpu='none', print('Processing non-recursively starting from', indir) recursive = False + if outdir is None: + print('No output directory given, processing in informational mode.', indir) + for fp in search_files(str(indir), recursive=recursive): fp = pathlib.PurePath(fp) if not fnmatch.fnmatch(fp, include): @@ -85,50 +90,63 @@ def main(indir, outdir, oformat='mp4', include='*', recursive=False, gpu='none', sys.exit(1) codec = video_stream['codec_name'] print(str(fp), "has codec", colored(codec, 'green')) - new_fp = outdir.joinpath(fp.with_suffix('.' + oformat).name) - if info: + if outdir is None: continue - if codec in SKIPPED_FORMATS: + if (codec in SKIPPED_CODECS) and not roku: continue if not fnmatch.fnmatch(fp, include): continue - print('From', str(fp), 'to', str(new_fp)) - if os.name == 'nt' and gpu == 'nvidia': - print(colored('Encoding with nVidia hardware acceleration', 'yellow')) - # https://slhck.info/video/2017/03/01/rate-control.html - # https://docs.nvidia.com/video-technologies/video-codec-sdk/ffmpeg-with-nvidia-gpu/ - # ffmpeg -h encoder=hevc_nvenc - print(ffmpeg.input(fp).output(str(new_fp), vsync=0, acodec='copy', map=0, vcodec='hevc_nvenc', **{'rc-lookahead': 25}, map_metadata=0, movflags='use_metadata_tags', preset='p5', spatial_aq=1, temporal_aq=1).run()) - if os.name == 'nt' and roku: + if roku: # http://www.rokoding.com/index.html - print(colored('Encoding with nVidia hardware acceleration', 'yellow')) + print(colored('Roku mode', 'magenta')) new_fp = outdir.joinpath(fp.with_suffix('.mkv').name) - print(ffmpeg.input(fp).output(str(new_fp), vsync=0, acodec='libopus', ab='96k', ac=2, vcodec='hevc_nvenc', **{'rc-lookahead': 25}, preset='p5', spatial_aq=1, temporal_aq=1, movflags='use_metadata_tags', **{'c:s': 'svt'}, map_metadata=0, ignore_chapters=1).compile()) + if os.path.exists(new_fp): + print(colored(str(new_fp) + ' exists', 'yellow')) + continue # Workaround for unsupported map in ffmpeg wrapper, we need '-map 0 -map -0:d' - command_line = ['ffmpeg', '-y', '-i', str(fp), '-ac', '2', '-ab', '96k', '-acodec', 'libopus', '-c:s', 'svt', '-ignore_chapters', '1', '-map_metadata', '0', '-movflags', 'use_metadata_tags', '-preset', 'p5', '-rc-lookahead', '25', '-spatial_aq', '1', '-temporal_aq', '1', '-vcodec', 'hevc_nvenc', '-vsync', '0', '-map', '0', '-map', '-0:d', str(new_fp)] - subprocess.run(command_line) - elif os.name == 'nt' and gpu == 'intel': - # ffmpeg -h encoder=hevc_qsv - print(ffmpeg.input(fp).output(str(new_fp), acodec='copy', map=0, vcodec='hevc_qsv', map_metadata=0, movflags='use_metadata_tags', **{'b:v': '3M'}, preset='slow').run()) - elif av1: - # ffmpeg -h encoder=libaom-av1 - print(colored('Encoding with experimental AV1 encoder', 'yellow')) - print('AV 1 codec:', colored(av1, 'yellow')) - if av1 == 'aom': - print(ffmpeg.input(fp).output(str(new_fp), pix_fmt='yuv420p', acodec='libopus', ab='96k', vcodec='libaom-av1', map_metadata=0, movflags='use_metadata_tags', crf=30).run()) - elif av1 == 'svt': - # ffmpeg -h encoder=libsvtav1 - print(ffmpeg.input(fp).output(str(new_fp), pix_fmt='yuv420p', acodec='libopus', ab='96k', vcodec='libsvtav1', qp=35, preset=5, map_metadata=0, movflags='use_metadata_tags').run()) - elif av1 == 'rav1e': - print('Rav1e not yet supported') - exit(0) + if notranscode: + command_line = ['ffmpeg', '-nostdin', '-i', str(fp), '-ac', '2', '-c:a', 'copy', '-c:v', 'copy', '-c:s', 'svt', '-ignore_chapters', '1', '-map_metadata', '0', '-movflags', 'use_metadata_tags', '-map', '0', '-map', '-0:d', str(new_fp)] + else: + command_line = ['ffmpeg', '-nostdin', '-i', str(fp), '-ac', '2', '-c:a', 'libopus', '-b:a', '96k', '-af', 'loudnorm=I=-16:LRA=11:TP=-1.5', '-c:v', 'copy', '-c:s', 'svt', '-ignore_chapters', '1', '-map_metadata', '0', '-movflags', 'use_metadata_tags', '-map', '0', '-map', '-0:d', str(new_fp)] + + print(command_line) + if run(command_line).returncode != 0: + exit(1) + else: + continue + else: + new_fp = outdir.joinpath(fp.with_suffix('.' + oformat).name) + if os.path.exists(new_fp): + print(colored(str(new_fp) + ' exists', 'yellow')) + continue + if os.name == 'nt' and gpu == 'nvidia': + print(colored('Encoding with nVidia hardware acceleration', 'yellow')) + # https://slhck.info/video/2017/03/01/rate-control.html + # https://docs.nvidia.com/video-technologies/video-codec-sdk/ffmpeg-with-nvidia-gpu/ + # ffmpeg -h encoder=hevc_nvenc + ffmpeg.input(fp).output(str(new_fp), vsync=0, acodec='copy', map=0, vcodec='hevc_nvenc', **{'rc-lookahead': 25}, map_metadata=0, movflags='use_metadata_tags', preset='p6', spatial_aq=1, temporal_aq=1).run() + elif av1: + # ffmpeg -h encoder=libaom-av1 + print(colored('Encoding with experimental AV1 encoder', 'yellow')) + print('AV 1 codec:', colored(av1, 'yellow')) + if av1 == 'aom': + ffmpeg.input(fp).output(str(new_fp), pix_fmt='yuv420p', acodec='libopus', ab='96k', vcodec='libaom-av1', map_metadata=0, movflags='use_metadata_tags', crf=28, preset='slow').run() + elif av1 == 'svt': + # ffmpeg -h encoder=libsvtav1 + ffmpeg.input(fp).output(str(new_fp), pix_fmt='yuv420p', acodec='libopus', ab='96k', vcodec='libsvtav1', qp=35, preset=5, map_metadata=0, movflags='use_metadata_tags').run() + elif av1 == 'rav1e': + print('Rav1e not yet supported') + exit(0) else: print(colored('Encoding with no hardware acceleration', 'yellow')) - print(ffmpeg.input(fp).output(str(new_fp), acodec='libopus', ab='64k', vcodec='libx265', map_metadata=0, movflags='use_metadata_tags', crf=20, preset='slow').run()) - saved = os.path.getsize(fp) - os.path.getsize(new_fp) - total += saved - print(colored(new_fp, 'green'), 'ready, saved', round(saved / 1024), 'KB') - print('Total saved', round(total / 1024 / 1024), 'MB') + # CRF 22 rationale: https://codecalamity.com/encoding-uhd-4k-hdr10-videos-with-ffmpeg/ + # ffmpeg -h encoder=libx265 + print(ffmpeg.input(fp).output(str(new_fp), acodec='libopus', ab='64k', vcodec='libx265', crf=22, preset='slow', map_metadata=0, movflags='use_metadata_tags').run()) + saved = os.path.getsize(fp) - os.path.getsize(new_fp) + assert saved > 0 + total += saved + print(colored(new_fp, 'green'), 'ready, saved', round(saved / 1024), 'KB') + print('Total saved', round(total / 1024 / 1024), 'MB') if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt index d2a0bd8..6bf357a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ click>=7.1.2 termcolor>=1.1.0 -pretty-errors>=1.2.19 +setuptools~=51.1.1 ffmpeg-python>=0.2.0 \ No newline at end of file