Skip to content

Instantly share code, notes, and snippets.

@SavinaRoja
Last active September 5, 2017 01:55
Show Gist options
  • Save SavinaRoja/a069e616de7b1d23fbe1dea24f2902b6 to your computer and use it in GitHub Desktop.
Save SavinaRoja/a069e616de7b1d23fbe1dea24f2902b6 to your computer and use it in GitHub Desktop.
Utilizing hyperarc representation for ffmpeg filtergraphs
#!/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