Last active
June 1, 2018 01:55
-
-
Save joeycato/7f1d78e06e32f30e53ee4bbb4dbc7d50 to your computer and use it in GitHub Desktop.
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
# 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