# syncroadtrip.py -A convenience script that will generate a split-screen of two synchronized videos | |
# For more context: http://joeycato.com/articles/reenacted-roadtrips/ | |
import re | |
import math | |
import os | |
from decimal import Decimal | |
from subprocess import check_output | |
# Note: This script has only been tested on a Win64 machine | |
# Create output folder if it doesn't already exist | |
if not os.path.exists('output'): | |
os.makedirs('output') | |
COMMANDS_OUTFILE = 'commands.txt' | |
BASE_FRAME_RATE = 60 | |
# Should be a higher multiple of the current frame rate ( improves precision ) | |
FRAME_RATE_INTERMEDIATE = 240 | |
FRAME_DURATION = Decimal('1')/Decimal(str(BASE_FRAME_RATE)) | |
FFMPEGCMD = 'ffmpeg -y' | |
ENCODERARGS = '-threads 0 -vcodec huffyuv -vsync cfr -an ' | |
DATA_2003 = { | |
'type': '2003', | |
'inputclip': ('./clip2003_%dfps_960x540.avi' % (BASE_FRAME_RATE)), | |
'outputclip': './final2003.avi' | |
} | |
DATA_2018 = { | |
'type': '2018', | |
'inputclip': ('./clip2018_%dfps_960x540.avi' % (BASE_FRAME_RATE)), | |
'outputclip': './final2018.avi' | |
} | |
LOGFILE = open(COMMANDS_OUTFILE, 'w') | |
def log(cmd): | |
"""Logs the command to file (for debugging)""" | |
print(cmd) | |
LOGFILE.write(cmd + '\n') | |
def exec_command(cmd): | |
"""Executes an ffmpeg command""" | |
log(' %s' % (cmd)) | |
result = check_output(cmd, shell=True) | |
return result.decode().strip() | |
def snap_to_frame(val): | |
"""Returns a value which is expected to align with the current framerate""" | |
if val == '0': | |
return 0 | |
return Decimal(math.floor(Decimal(val) / FRAME_DURATION)) * FRAME_DURATION | |
def parse_sync_points(data): | |
"""Parses the text file which contains a list of timestamps representing the sync points""" | |
markers = [] | |
metadatafile = './syncdata%s.txt' % (data['type']) | |
inputfile = open(metadatafile) | |
log("Parsing " + str(data['type'])) | |
numlines = 0 | |
for line in inputfile: | |
results = re.findall(r"[-+]?\d*\.\d+|\d+", line) | |
rlen = len(results) | |
if rlen > 0: | |
timestamp = results[0] | |
markers.append({ | |
'startTime': snap_to_frame(timestamp) | |
}) | |
numlines = numlines + 1 | |
num_markers = len(markers) | |
idx = 0 | |
for marker in markers: | |
print(marker) | |
if idx < num_markers - 1: | |
nextmarker = markers[idx+1] | |
marker['dur_rounded'] = snap_to_frame(nextmarker['startTime'] - marker['startTime']) | |
idx = idx + 1 | |
data['markers'] = markers | |
# Slows down the shortest segment so that it matches the same duration as its counterpart segment | |
def resize_segment(baseclip, newoutclip, baseclip_duration, target_duration): | |
"""Slows down the current segment so that its duration matches its counterpart""" | |
log("Scaling %s..." % (newoutclip)) | |
scale_factor = target_duration / baseclip_duration | |
log(" baseclip_duration=%s" % (baseclip_duration)) | |
log(" target_duration=%s" % (target_duration)) | |
log(" scale_factor=%s" % (scale_factor)) | |
exec_command('%s -i output/%s -vf "setpts=(%s)*PTS" %s output/%s' % \ | |
(FFMPEGCMD, baseclip, scale_factor, ENCODERARGS, newoutclip)) | |
def extract_clips(data): | |
"""Extracts all video segments from the current clip ( and scales the duration if necessary )""" | |
log("Extracting clips...") | |
idx = 1 | |
markers = data['markers'] | |
inputclip = data['inputclip'] | |
segment_prefix = 'segment_' + data['type'] + '_' | |
listfilename = segment_prefix + 'list.txt' | |
listfile = open('output/' + listfilename, 'w') | |
log("ffmpeg commands for %s" % (inputclip)) | |
num_markers = len(markers) | |
for marker in markers: | |
if marker.get('dur_rounded') != None: | |
outclip = segment_prefix + str(idx) + '.avi' | |
baseclip_duration = marker['dur_rounded'] | |
start_time = marker['startTime'] | |
log("Extracting %s...[%.1f%%%%]" % (outclip, 100*(idx)/num_markers)) | |
exec_command("%s -ss %s -t %s -i %s %s -r %s output/%s" % \ | |
(FFMPEGCMD, start_time, baseclip_duration, inputclip, ENCODERARGS, FRAME_RATE_INTERMEDIATE, outclip)) | |
scale_factor = marker.get('scale_factor') | |
if scale_factor != None: | |
newoutclip = segment_prefix + str(idx) + '_scaled.avi' | |
target_duration = marker['target_duration'] | |
resize_segment(outclip, newoutclip, baseclip_duration, target_duration) | |
outclip = newoutclip | |
marker['outclip'] = outclip | |
listfile.write("file '%s'\n" % (outclip)) | |
idx = idx + 1 | |
listfile.close() | |
# create list file ( used for concatenation ) | |
log("Concatenating...") | |
exec_command('%s -safe 0 -f concat -i output/%s %s -r %s %s' % (FFMPEGCMD, listfilename, ENCODERARGS, BASE_FRAME_RATE, data['outputclip'])) | |
# compare extracted metadata between both clips and inject a scale multiplier to correct | |
def sync_clips(data_before, data_after): | |
"""Compares the two extracted video segments and stores additional metadata""" | |
log("Syncing up...") | |
markers_before = data_before['markers'] | |
markers_after = data_after['markers'] | |
if len(markers_before) != len(markers_after): | |
print("Mismatch!") | |
return | |
num_markers = len(markers_before) | |
for idx in range(0, num_markers-1): | |
marker_before = markers_before[idx] | |
marker_after = markers_after[idx] | |
secs_before = marker_before.get('dur_rounded') | |
secs_after = marker_after.get('dur_rounded') | |
if ((secs_before and secs_after) and (secs_before != secs_after)): | |
if secs_before < secs_after: | |
marker_before['scale_factor'] = secs_after / secs_before | |
marker_before['target_duration'] = marker_after['dur_rounded'] | |
else: | |
marker_after['scale_factor'] = secs_before / secs_after | |
marker_after['target_duration'] = marker_before['dur_rounded'] | |
else: | |
return | |
return | |
parse_sync_points(DATA_2003) | |
parse_sync_points(DATA_2018) | |
sync_clips(DATA_2003, DATA_2018) | |
extract_clips(DATA_2003) | |
extract_clips(DATA_2018) | |
# Generate split screen | |
exec_command('%s -i final2003.avi -i final2018.avi %s -filter_complex vstack final_splitscreen.avi' % (FFMPEGCMD, ENCODERARGS)) | |
log('Done!') | |
LOGFILE.close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment