Last active
August 31, 2017 12:48
-
-
Save SavinaRoja/a6fc46e02faecd9495fb7010f4263176 to your computer and use it in GitHub Desktop.
Some updates to Sebastien Charlemagne's code for flexibility and maintainability
This file contains hidden or 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. | |
-k --keep-intermediates Don't delete all intermediate (temporary) files. | |
-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] | |
-m --multiprocess=NPOOL Use a multiprocess pool to produce intermediates from | |
inputs in parallel. Can produce greater CPU | |
saturation and give shorter execution times. Provide | |
an integer value, 2 is a good start. [default: 0] | |
Authors: | |
Paul Barton (SavinaRoja) | |
Sebastien Charlemagne (scharlem) | |
""" | |
#NOTES: | |
#Significant thanks to Sebastien for doing the heavy lifting to configure the | |
#scale filter. This script is a little slower as it spends a little more time | |
#in the Python environment than FFMPEG. Let's review the changes: | |
# * Ultimately this should be more maintainable. It was a real bear to dig | |
# into the first version (but it was technically brilliant!) | |
# * I gave it a flexible interface with configurable options, this will help to | |
# easily test new options. The output can be named | |
# * This script ought to be fully cross platform. | |
# * If you don't like my defaults, you can change them easily! | |
# | |
#On lossless, I think that unless the disk space is a severe limitation, it | |
#should be worth doing lossless output. Downstream operations can then all be | |
#performed at that level before rendering to whatever target container/codec | |
#and there will only ever be one quality-reducing step... | |
#stdlib imports | |
from collections import OrderedDict | |
import json | |
import os | |
import shutil | |
import subprocess | |
from multiprocessing import Pool | |
from functools import partial | |
#External library imports | |
from docopt import docopt # For the easy interface | |
__version__ = '0.1.0' | |
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]) | |
intermediates_dir = os.path.join(base_dir, 'intermediates') | |
if not os.path.exists(intermediates_dir): | |
os.mkdir(intermediates_dir) | |
#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) | |
intermediates = [] | |
commands = [] | |
media = json_inp['mediaSources'] | |
for index, start_point in enumerate(media): | |
index_str = str(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)) | |
#Create name and paths for intermediate file | |
intermediate_name = 'intermediate-{}.mkv'.format(index) | |
intermediate_filepath = os.path.join(intermediates_dir, intermediate_name) | |
intermediates.append(intermediate_filepath) | |
#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 | |
#duration_str = str(duration) | |
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, 'index': str(index)}) | |
to_intermediate_cmd = ['ffmpeg', | |
'-ss', in_stream_start, '-t', str(duration), | |
'-i', inptpath, | |
'-lavfi'] | |
if is_video: | |
#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: | |
filter_add = """\ | |
[short];nullsrc=size={size}:duration={duration}[BG];[BG][short]overlay\ | |
""".format(**{'size': args['--size'], 'duration': str(duration), 'index': str(index)}) | |
my_filter += filter_add | |
#The input is not a video, thus a still image | |
else: | |
#insert the loop on the still image input | |
to_intermediate_cmd = to_intermediate_cmd[:1] + ['-loop', '1'] + to_intermediate_cmd[1:] | |
#Now that we have completed manipulating the video filter, add it to the command | |
#my_filter = my_filter + '"' | |
to_intermediate_cmd.append(my_filter) | |
#Time to add our output specifications | |
output_args = ['-c:a', 'copy', | |
'-pix_fmt', args['--pix-format'], | |
'-c:v', 'libx264', | |
'-crf', args['--crf'], | |
'-preset', args['--preset'], | |
intermediate_filepath, '-y'] | |
to_intermediate_cmd += output_args | |
commands.append(to_intermediate_cmd) | |
#subprocess.run(to_intermediate_cmd) | |
my_run = partial(subprocess.run, | |
stdout=subprocess.DEVNULL, | |
stderr=subprocess.DEVNULL) | |
if args['--multiprocess'] not in ['0', '1']: | |
n = int(args['--multiprocess']) | |
with Pool(n) as p: | |
for command in commands: | |
p.apply_async(my_run, (command,)) | |
p.close() | |
p.join() | |
else: | |
for command in commands: | |
my_run(command) | |
#So now that we have pegged the inputs to the target specification, we will | |
#now concatenate them with the concatenation demuxer then lay on the audio | |
demuxer_file = os.path.join(intermediates_dir, 'demux.txt') | |
tmp_output = os.path.join(intermediates_dir, 'tmp.mkv') | |
#Write the concat demuxer file then use it | |
with open(demuxer_file, 'w') as outp: | |
for item in intermediates: | |
outp.write('file ' + item + '\n') | |
subprocess.run(['ffmpeg', | |
'-f', 'concat', '-safe', '0', | |
'-i', demuxer_file, | |
'-c', 'copy', '-shortest', | |
tmp_output, '-y']) | |
#Take the audio-less temp file and the audio to produce final input | |
subprocess.run(['ffmpeg', '-i', tmp_output, '-i', audio_file_path, | |
'-acodec', 'copy', | |
'-vcodec', 'copy', | |
'-shortest', args['<output>'], '-y']) | |
#If we got this far, then things probably went all right! Time to tear down | |
#the intermediates directory unless disabled | |
if not args['--keep-intermediates']: | |
shutil.rmtree(intermediates_dir) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment