Last active
September 5, 2017 01:55
-
-
Save SavinaRoja/a069e616de7b1d23fbe1dea24f2902b6 to your computer and use it in GitHub Desktop.
Utilizing hyperarc representation for ffmpeg filtergraphs
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 | |
""" | |
Concat - a tool for efficiently concatenating video inputs and combining with | |
an audio file | |
Usage: | |
concat <json-input> [options] <output> | |
concat (-h | --help) | |
concat --version | |
Options: | |
-a --audio=AUDIOPATH Specify a file path to the audio file other than the | |
default ./audio.mp3 relative to JSON input. | |
-c --crf=CRF Set the libx264 CRF value. [default: 0] | |
-p --preset=PRESET Set the libx264 --preset [default: ultrafast] | |
-P --pix-format=PIXFMT Set the pixel format for the output video stream. | |
[default: yuv444p] | |
-s --size=WIDTHXHEIGHT Set the output video size in width x height, like | |
"1280x720". [default: 1280x720] | |
Authors: | |
Paul Barton (SavinaRoja) | |
Sebastien Charlemagne (scharlem) | |
""" | |
#stdlib imports | |
from collections import OrderedDict | |
import json | |
import os | |
import shutil | |
import subprocess | |
from collections import namedtuple | |
#External library imports | |
from docopt import docopt # For the easy interface | |
#Fundamental concept: the ffmpeg filtergraph is a hypergraph | |
#It is sufficient to denote your filters as hyperarcs (directional hyperedges) | |
#These hyperarcs connect to the input/output nodes of the filtergraph | |
#This code is meant to explore the syntax enabled by the hyperarc representation | |
#for ease of use with FFMPEG filtergraphs | |
hyperarc = namedtuple('HyperArc', ['innodes', 'outnode', 'filterstr']) | |
__version__ = '0.1.0' | |
def format_hyperarcs(hyperarcs | |
strings = [] | |
for arc in hyperarcs: | |
inputs = ' '.join(arc.innodes) | |
strings.append(inputs + ' ' + arc.filterstr + arc.outnode) | |
return '; '.join(strings) | |
if __name__ == '__main__': | |
args = docopt(__doc__, version='Concat {}'.format(__version__)) | |
WIDTH, HEIGHT = args['--size'].split('x') | |
#All file paths in the JSON will be considered to be relative to JSON file | |
base_dir = os.path.abspath(os.path.split(args['<json-input>'])[0]) | |
#the audio.mp3 file is assumed to be at the same level as the .json unless | |
#otherwise specified | |
if args['--audio'] is not None: | |
audio_file_path = os.path.abspath(args['--audio']) | |
else: | |
audio_file_path = os.path.join(base_dir, 'audio.mp3') | |
#Parse the JSON with OrderedDict to easily keep the order | |
with open(args['<json-input>']) as inp: | |
json_inp = json.load(inp, object_pairs_hook=OrderedDict) | |
#The command we will build up to run in the end | |
command = ['ffmpeg',] | |
hyperarcs = [] | |
concat_arc_srcs = [] | |
media = json_inp['mediaSources'] | |
for index, start_point in enumerate(media): | |
#nodes.append('[{}]'.format(index)) | |
attrs = media[start_point]['attributes'] | |
#Perform some basic filename and path operations | |
inptname = attrs['fileName'] | |
inptname_root, inptname_ext = os.path.splitext(inptname) | |
#create a normalized, absolute path | |
inptpath = os.path.normpath(os.path.join(base_dir, inptname)) | |
#Grab and do some calculations with the attributes | |
in_stream_start = str(media[start_point]['attributes']['trim'] / 1000.0) | |
duration = media[start_point]['attributes']['duration'] / 1000.0 | |
is_video = media[start_point]['attributes']['animated'] | |
my_filter = """\ | |
scale=(iw*sar)*min({width}/(iw*sar)\,{height}/ih):ih*min({width}/(iw*sar)\,\ | |
{height}/ih),pad={width}:{height}:({width}-iw*min({width}/iw\,{height}/ih))/2:(\ | |
{height}-ih*min({width}/iw\,{height}/ih))/2\ | |
""".format(width=WIDTH, height=HEIGHT) | |
input_args = ['-ss', in_stream_start, '-t', str(duration), | |
'-i', inptpath] | |
if not is_video: | |
input_args = ['-loop', '1'] + input_args | |
i_arc_out = '[{}_final]'.format(index) | |
else: | |
#use ffprobe to get duration of the stream | |
ffprobe_duration = subprocess.check_output(['ffprobe', | |
'-v','error', | |
'-show_entries', 'format=duration', | |
'-of', 'default=noprint_wrappers=1:nokey=1', | |
inptpath]) | |
if ffprobe_duration[0:3] == "N/A": #Handle some odd cases where duration is unknown | |
actualDuration=0 | |
else: | |
ffprobe_duration=float(ffprobe_duration) | |
#Stream is shorter than expected. Just adjust myfilter to have it the right length | |
#this is done by creating a null src with the right duration then overlaying it | |
#with our shorter stream. the Overlay filter is responsible to still the last | |
#frame | |
if ffprobe_duration < duration: | |
null = "nullsrc=size={size}:duration={duration}".format(size=args['--size'], | |
duration=duration) | |
j_arc = hyperarc(innodes=[], | |
outnode='[BG]', | |
filterstr=null) | |
k_arc = hyperarc(innodes=['[BG]', '[{}_scaled]'.format(index)], | |
outnode='[{}_final]'.format(index), | |
filterstr='overlay') | |
hyperarcs.append(j_arc) | |
hyperarcs.append(k_arc) | |
i_arc_out = '[{}_scaled]'.format(index) | |
else: | |
i_arc_out = '[{}_final]'.format(index) | |
i_arc = hyperarc(innodes=['[{}]'.format(index)], | |
outnode=i_arc_out, | |
filterstr=my_filter) | |
hyperarcs.append(i_arc) | |
concat_arc_srcs.append('[{}_final]'.format(index)) | |
command += input_args | |
concat_arc = hyperarc(innodes=concat_arc_srcs, | |
outnode='', | |
filterstr='concat=n={}:unsafe=1'.format(len(concat_arc_srcs))) | |
hyperarcs.append(concat_arc) | |
command += ['-i', audio_file_path] | |
command = command + ['-filter_complex', | |
format_hyperarcs(hyperarcs)] | |
command += ['-map', str(len(media)) + ':a', | |
'-c:a', 'copy', | |
'-pix_fmt', args['--pix-format'], | |
'-c:v', 'libx264', | |
'-crf', args['--crf'], | |
'-preset', args['--preset'], | |
'-shortest', | |
args['<output>'], '-y'] | |
#print(' '.join(command)) | |
#print(command) | |
subprocess.run(command) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment