Last active
August 24, 2020 05:49
-
-
Save rdelcueto/704ecb94b675e8a6f68bcb7d65c46ce3 to your computer and use it in GitHub Desktop.
ffmpeg encoding & processing script through docker image
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
ffmpeg encoding & processing script through docker image: see https://github.com/jrottenberg/ffmpeg | |
""" | |
__author__ = 'Rodrigo Gonzalez del Cueto' | |
__copyright__ = 'Copyright 2020, ffmpeg processor' | |
__credits__ = ['Rodrigo Gonzalez del Cueto'] | |
__license__ = '{GNU Lesser General Public License version 3}' | |
__version__ = '0.1.5' | |
__email__ = '[email protected]' | |
import os | |
import sys | |
import subprocess | |
import itertools | |
from collections import OrderedDict | |
import toml | |
import argparse | |
import glob | |
# Script debugging variables | |
debug = True | |
debugprint = print if debug else lambda *a, **k: None | |
# Script default configuration | |
default_config_toml = """ | |
# Default configuration TOML file | |
title = "Docker ffmpeg automation script configuration" | |
[docker] | |
uid = 1000 | |
gid = 65536 | |
device-bind = "--device /dev/dri:/dev/dri" | |
cpu-config = "--cpuset-cpus=0,1" | |
mem-config = "-m 1g" | |
image-tag = "jrottenberg/ffmpeg:snapshot-vaapi" | |
[ffmpeg.hw-video] | |
codec = "hevc_vaapi" | |
cqp = 25 | |
#scale_w = 1280 | |
#scale_h = 720 | |
[ffmpeg.video] | |
codec = "libx265" | |
tune-params = "fastdecode -preset fast" | |
crf = 22 | |
#scale_w = 1280 | |
#scale_h = 720 | |
[ffmpeg.stabilization.vidstabdetect] | |
shakiness = 8 | |
accuracy = 10 | |
stepsize = 4 | |
[ffmpeg.stabilization.vidstabtransform] | |
smoothing = 6 | |
optalgo = "gauss" | |
maxshift = -1 | |
maxangle = -1 | |
crop = "keep" | |
optzoom = 2 | |
zoomspeed = 0.2 | |
interpol = "bicubic" | |
[ffmpeg.stabilization.unsharp] | |
luma_msize_x = 3 | |
luma_msize_y = 3 | |
luma_amount = 0.8 | |
chroma_msize_x = 3 | |
chroma_msize_y = 3 | |
chroma_amount = 0.4 | |
[ffmpeg.audio] | |
#codec = "copy" | |
#bitrate-param = "" | |
codec = "libvorbis" | |
bitrate-param = "-qscale:a 4" | |
[ffmpeg.output] | |
container = "mp4" | |
""" | |
def parse_config (args): | |
if args.config_file is None: | |
parsed_toml = toml.loads(default_config_toml) | |
else: | |
if not os.path.isfile(args.config_file): | |
debugprint ("Config file doesn't exist") | |
return None | |
else: | |
debugprint ("Parsing TOML config file {}".format(args.config_file)) | |
parsed_toml = toml.load(args.config_file) | |
return parsed_toml | |
def execute_docker_process (args, config, ffmpeg_cmd, cwd, environment): | |
cmd = ("docker run --user {}:{}".format(config['docker']['uid'], config['docker']['gid']) + | |
" {}".format(config['docker']['device-bind']) + | |
" {}".format(config['docker']['cpu-config']) + | |
" {}".format(config['docker']['mem-config']) + | |
" -v {}:{} -w {}".format(cwd, cwd, cwd) + | |
" {}".format(config['docker']['image-tag']) + | |
" {}".format(ffmpeg_cmd)) | |
if args.verbose is True: | |
print (cmd) | |
if args.dry_run is False: | |
cmd = cmd.split () | |
docker_ffmpeg_process = subprocess.Popen (cmd, env=environment, stdout=subprocess.PIPE) | |
out, err = docker_ffmpeg_process.communicate () | |
print (out) | |
def process_input_files (args, config, input_files): | |
# Set PATH | |
environment = os.environ.copy () | |
environment["PATH"] = "/usr/sbin:/usr/bin:" + environment["PATH"] | |
cwd = os.getcwd () | |
for file in input_files: | |
print ("Processing {}...".format(file)) | |
if args.stabilize_video is False: | |
if args.hwaccel == 'off': | |
# Single Pass | |
ffmpeg_cmd = get_single_pass_cmd (args, config, file, args.output_suffix) | |
execute_docker_process (args, config, ffmpeg_cmd, cwd, environment) | |
else: | |
# Single Pass with Hardware acceleration decoding & encoding | |
ffmpeg_cmd = get_vaapi_single_pass_cmd (args, config, file, args.output_suffix) | |
execute_docker_process (args, config, ffmpeg_cmd, cwd, environment) | |
else: | |
if args.hwaccel == 'off': | |
# First Pass | |
ffmpeg_cmd = get_stabilized_1st_pass_cmd (args, config, file) | |
execute_docker_process (args, config, ffmpeg_cmd, cwd, environment) | |
# Second Pass | |
ffmpeg_cmd = get_stabilized_2nd_pass_cmd (args, config, file, args.output_suffix + "_stabilized") | |
execute_docker_process (args, config, ffmpeg_cmd, cwd, environment) | |
else: | |
# First Pass with Hardware acceleration decoding & encoding | |
ffmpeg_cmd = get_vaapi_stabilized_1st_pass_cmd (args, config, file) | |
execute_docker_process (args, config, ffmpeg_cmd, cwd, environment) | |
# Second Pass with Hardware acceleration decoding & encoding | |
ffmpeg_cmd = get_vaapi_stabilized_2nd_pass_cmd (args, config, file, args.output_suffix + "_stabilized") | |
execute_docker_process (args, config, ffmpeg_cmd, cwd, environment) | |
def get_single_pass_cmd (args, config, input_file, output_suffix): | |
input_filename_with_extension = os.path.basename(input_file) | |
output_filename = os.path.splitext(input_filename_with_extension)[0] | |
video_filters = [] | |
if config['ffmpeg']['video'].get('scale_w') and config['ffmpeg']['video'].get('scale_h'): | |
scale = "scale=w={}:h={}".\ | |
format(config['ffmpeg']['video']['scale_w'], config['ffmpeg']['video']['scale_h']) | |
video_filters.append (scale) | |
video_filters = ','.join(video_filters) if video_filters else None | |
cmd = (" -i {}".format(input_file) + | |
" -c:v {}".format(config['ffmpeg']['video']['codec']) + | |
((" -vf " + video_filters) if video_filters else "") + | |
" -tune {}".format(config['ffmpeg']['video']['tune-params']) + | |
" -crf {}".format(config['ffmpeg']['video']['crf']) + | |
" -c:a {}".format(config['ffmpeg']['audio']['codec']) + | |
" {}".format(config['ffmpeg']['audio']['bitrate-param']) + | |
(" -y " if args.force_overwrite else " -n ") + | |
output_filename + output_suffix + '.' + config['ffmpeg']['output']['container']) | |
return cmd | |
def get_stabilized_1st_pass_cmd (args, config, input_file): | |
input_filename_with_extension = os.path.basename(input_file) | |
output_filename = os.path.splitext(input_filename_with_extension)[0] | |
video_filters = [] | |
if config['ffmpeg']['video'].get('scale_w') and config['ffmpeg']['video'].get('scale_h'): | |
scale = "scale=w={}:h={}".\ | |
format(config['ffmpeg']['video']['scale_w'], config['ffmpeg']['video']['scale_h']) | |
video_filters.append (scale) | |
vidstabdetect = "vidstabdetect=shakiness={}:accuracy={}:stepsize={}:result={}".\ | |
format(config['ffmpeg']['stabilization']['vidstabdetect']['shakiness'], | |
config['ffmpeg']['stabilization']['vidstabdetect']['accuracy'], | |
config['ffmpeg']['stabilization']['vidstabdetect']['stepsize'], | |
(output_filename + '_vidstabdetect.trf')) | |
video_filters.append (vidstabdetect) | |
video_filters = ','.join(video_filters) if video_filters else None | |
cmd = (" -i {}".format(input_file) + | |
((" -vf " + video_filters) if video_filters else "") + | |
" -an -f null -") | |
return cmd | |
def get_stabilized_2nd_pass_cmd (args, config, input_file, output_suffix): | |
input_filename_with_extension = os.path.basename(input_file) | |
output_filename = os.path.splitext(input_filename_with_extension)[0] | |
video_filters = [] | |
if config['ffmpeg']['video'].get('scale_w') and config['ffmpeg']['video'].get('scale_h'): | |
scale = "scale=w={}:h={}".\ | |
format(config['ffmpeg']['video']['scale_w'], config['ffmpeg']['video']['scale_h']) | |
video_filters.append (scale) | |
vidstabtransform = "vidstabtransform=smoothing={}:optalgo={}:maxshift={}:maxangle={}:crop={}:optzoom={}:zoomspeed={}:interpol={}:input={}".\ | |
format(config['ffmpeg']['stabilization']['vidstabtransform']['smoothing'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['optalgo'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['maxshift'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['maxangle'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['crop'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['optzoom'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['zoomspeed'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['interpol'], | |
(output_filename + '_vidstabdetect.trf')) | |
video_filters.append (vidstabtransform) | |
unsharp = "unsharp={}:{}:{}:{}:{}:{}".\ | |
format(config['ffmpeg']['stabilization']['unsharp']['luma_msize_x'], | |
config['ffmpeg']['stabilization']['unsharp']['luma_msize_y'], | |
config['ffmpeg']['stabilization']['unsharp']['luma_amount'], | |
config['ffmpeg']['stabilization']['unsharp']['chroma_msize_x'], | |
config['ffmpeg']['stabilization']['unsharp']['chroma_msize_y'], | |
config['ffmpeg']['stabilization']['unsharp']['chroma_amount']) | |
video_filters.append (unsharp) | |
video_filters = ','.join(video_filters) if video_filters else None | |
cmd = (" -i {}".format(input_file) + | |
((" -vf " + video_filters) if video_filters else "") + | |
" -c:v {}".format(config['ffmpeg']['video']['codec']) + | |
" -tune {}".format(config['ffmpeg']['video']['tune-params']) + | |
" -crf {}".format(config['ffmpeg']['video']['crf']) + | |
" -c:a {}".format(config['ffmpeg']['audio']['codec']) + | |
" {}".format(config['ffmpeg']['audio']['bitrate-param']) + | |
(" -y " if args.force_overwrite else " -n ") + | |
output_filename + output_suffix + '.' + config['ffmpeg']['output']['container']) | |
return cmd | |
def get_vaapi_single_pass_cmd (args, config, input_file, output_suffix): | |
input_filename_with_extension = os.path.basename(input_file) | |
output_filename = os.path.splitext(input_filename_with_extension)[0] | |
video_filters = [] | |
if config['ffmpeg']['hw-video'].get('scale_w') and config['ffmpeg']['hw-video'].get('scale_h'): | |
scale = "scale_vaapi=w={}:h={}".\ | |
format(config['ffmpeg']['hw-video']['scale_w'], config['ffmpeg']['hw-video']['scale_h']) | |
video_filters.append (scale) | |
video_filters = ','.join(video_filters) if video_filters else None | |
cmd = (" -hwaccel vaapi -hwaccel_output_format vaapi" + | |
" -i {}".format(input_file) + | |
((" -vf " + video_filters) if video_filters else "") + | |
" -c:v {}".format(config['ffmpeg']['hw-video']['codec']) + | |
" -qp {}".format(config['ffmpeg']['hw-video']['cqp']) + | |
" -c:a {}".format(config['ffmpeg']['audio']['codec']) + | |
" {}".format(config['ffmpeg']['audio']['bitrate-param']) + | |
(" -y " if args.force_overwrite else " -n ") + | |
output_filename + output_suffix + '.' + config['ffmpeg']['output']['container']) | |
return cmd | |
def get_vaapi_stabilized_1st_pass_cmd (args, config, input_file): | |
input_filename_with_extension = os.path.basename(input_file) | |
output_filename = os.path.splitext(input_filename_with_extension)[0] | |
video_filters = [] | |
if config['ffmpeg']['hw-video'].get('scale_w') and config['ffmpeg']['hw-video'].get('scale_h'): | |
scale = "scale_vaapi=w={}:h={}".\ | |
format(config['ffmpeg']['hw-video']['scale_w'], config['ffmpeg']['hw-video']['scale_h']) | |
video_filters.append (scale) | |
video_filters.append ("hwdownload") | |
video_filters.append ("format=nv12") | |
vidstabdetect = "vidstabdetect=shakiness={}:accuracy={}:stepsize={}:result={}".\ | |
format(config['ffmpeg']['stabilization']['vidstabdetect']['shakiness'], | |
config['ffmpeg']['stabilization']['vidstabdetect']['accuracy'], | |
config['ffmpeg']['stabilization']['vidstabdetect']['stepsize'], | |
(output_filename + '_vidstabdetect.trf')) | |
video_filters.append (vidstabdetect) | |
video_filters = ','.join(video_filters) if video_filters else None | |
cmd = (" -hwaccel vaapi -hwaccel_output_format vaapi" + | |
" -i {}".format(input_file) + | |
((" -vf " + video_filters) if video_filters else "") + | |
" -an -f null -") | |
return cmd | |
def get_vaapi_stabilized_2nd_pass_cmd (args, config, input_file, output_suffix): | |
input_filename_with_extension = os.path.basename(input_file) | |
output_filename = os.path.splitext(input_filename_with_extension)[0] | |
video_filters = [] | |
if config['ffmpeg']['hw-video'].get('scale_w') and config['ffmpeg']['hw-video'].get('scale_h'): | |
scale = "scale_vaapi=w={}:h={}".\ | |
format(config['ffmpeg']['hw-video']['scale_w'], config['ffmpeg']['hw-video']['scale_h']) | |
video_filters.append (scale) | |
video_filters.append ("hwdownload") | |
video_filters.append ("format=nv12") | |
vidstabtransform = "vidstabtransform=smoothing={}:optalgo={}:maxshift={}:maxangle={}:crop={}:optzoom={}:zoomspeed={}:interpol={}:input={}".\ | |
format(config['ffmpeg']['stabilization']['vidstabtransform']['smoothing'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['optalgo'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['maxshift'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['maxangle'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['crop'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['optzoom'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['zoomspeed'], | |
config['ffmpeg']['stabilization']['vidstabtransform']['interpol'], | |
(output_filename + '_vidstabdetect.trf')) | |
video_filters.append (vidstabtransform) | |
unsharp = "unsharp={}:{}:{}:{}:{}:{}".\ | |
format(config['ffmpeg']['stabilization']['unsharp']['luma_msize_x'], | |
config['ffmpeg']['stabilization']['unsharp']['luma_msize_y'], | |
config['ffmpeg']['stabilization']['unsharp']['luma_amount'], | |
config['ffmpeg']['stabilization']['unsharp']['chroma_msize_x'], | |
config['ffmpeg']['stabilization']['unsharp']['chroma_msize_y'], | |
config['ffmpeg']['stabilization']['unsharp']['chroma_amount']) | |
video_filters.append (unsharp) | |
video_filters.append ("hwupload") | |
video_filters = ','.join(video_filters) if video_filters else None | |
cmd = (" -hwaccel vaapi -hwaccel_output_format vaapi" + | |
" -i {}".format(input_file) + | |
((" -vf " + video_filters) if video_filters else "") + | |
" -c:v {}".format(config['ffmpeg']['hw-video']['codec']) + | |
" -qp {}".format(config['ffmpeg']['hw-video']['cqp']) + | |
" -c:a {}".format(config['ffmpeg']['audio']['codec']) + | |
" {}".format(config['ffmpeg']['audio']['bitrate-param']) + | |
(" -y " if args.force_overwrite else " -n ") + | |
output_filename + output_suffix + '.' + config['ffmpeg']['output']['container']) | |
return cmd | |
def parse_input_files (args): | |
if args.input_directory is None and args.input_files == [None]: | |
print ("Error: No valid input defined!\n", file=sys.stderr) | |
return None | |
input_files_from_directories = [] | |
if args.input_directory is not None: | |
glob_extensions = [(lambda x: "/*.{}".format(x))(file_type) for file_type in args.input_file_type] | |
input_files_from_directories.extend(glob.glob(directory + ext) for ext in glob_extensions for directory in args.input_directory) | |
input_files_from_directories = list (itertools.chain.from_iterable(input_files_from_directories)) | |
if args.input_files is not None: | |
input_files = args.input_files | |
else: | |
input_files = [] | |
# Merge | |
input_files = list (OrderedDict.fromkeys (input_files + input_files_from_directories)) | |
if args.verbose is not None: | |
print (input_files) | |
return input_files | |
def check_input_file (file): | |
if not os.path.isfile(file): | |
print("ERROR - Input file does not exist: {}".format(file), file=sys.stderr) | |
return None | |
return file | |
def check_input_directory (dir): | |
if not os.path.isdir(dir): | |
print("ERROR - Input directory does not exist: {}".format(dir), file=sys.stderr) | |
return None | |
return dir | |
def main(): | |
# Directory | |
parser = argparse.ArgumentParser (description='Encode & process video files using ffmpeg from a docker container image') | |
parser.add_argument ('-i', '--input-files', | |
nargs='?', action='append', | |
help="files to process", type=check_input_file) | |
parser.add_argument ('-d', '--input-directory', | |
nargs='?', action='append', | |
help="directories to process", type=check_input_directory) | |
parser.add_argument ('-t', '--input-file-type', | |
nargs='?', action='append', | |
default = ["mp4"], | |
help="file types to process. Default: mp4") | |
parser.add_argument ('--output-suffix', | |
default="_converted", | |
help="output file suffix. Default = _converted") | |
parser.add_argument ('-A', '--hwaccel', | |
choices=['off', 'vaapi'], | |
default='vaapi', | |
help="Use specified hardware acceleration for decoding and encoding. Default = vaapi") | |
parser.add_argument ('-S', '--stabilize-video', | |
action='store_true', | |
default=False, | |
help="Analyze video stabilization/deshaking. Perform pass 1 with vidstabdetect filter, and vidstabtransform filter for pass 2. ") | |
parser.add_argument ('-C', '--config-file', | |
default=None, | |
help="Config file to define ffmpeg parameters") | |
parser.add_argument ('-n', '--dry-run', | |
action='store_true', | |
default=False, | |
help="perform a trial run with no processing of video files") | |
parser.add_argument ('-v', '--verbose', | |
action='store_true', | |
default=False, | |
help="increase verbosity") | |
parser.add_argument ('-f', '--force-overwrite', | |
action='store_true', | |
default=False, | |
help="overwrite output files") | |
args = parser.parse_args () | |
debugprint (args) | |
config = parse_config (args) | |
if config is None: | |
print ("Error reading config file") | |
sys.exit (1) | |
input_files = parse_input_files (args) | |
if input_files is None: | |
print ("Error: No input files to process!") | |
sys.exit (1) | |
process_input_files (args, config, input_files) | |
sys.exit (0) | |
if __name__ == "__main__": | |
main () |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment