Skip to content

Instantly share code, notes, and snippets.

@rdelcueto
Last active August 24, 2020 05:49
Show Gist options
  • Save rdelcueto/704ecb94b675e8a6f68bcb7d65c46ce3 to your computer and use it in GitHub Desktop.
Save rdelcueto/704ecb94b675e8a6f68bcb7d65c46ce3 to your computer and use it in GitHub Desktop.
ffmpeg encoding & processing script through docker image
#!/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__ = 'rdelcueto@gmail.com'
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