Skip to content

Commit

Permalink
Release 0.4.4
Browse files Browse the repository at this point in the history
  • Loading branch information
varnav committed Feb 22, 2021
1 parent 4182331 commit 4be8a37
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 57 deletions.
16 changes: 10 additions & 6 deletions README.md
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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/)
4 changes: 2 additions & 2 deletions build_ffmpeg.sh
Expand Up @@ -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
Expand Down
114 changes: 66 additions & 48 deletions filmcompress.py
Expand Up @@ -3,22 +3,24 @@
import fnmatch
import os
import pathlib
import subprocess
from subprocess import run
import sys
from typing import Iterable
import click
from termcolor import colored
# 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)):
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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__':
Expand Down
2 changes: 1 addition & 1 deletion 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

0 comments on commit 4be8a37

Please sign in to comment.