Skip to content

Instantly share code, notes, and snippets.

@joeycato
Last active June 1, 2018 01:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joeycato/7f1d78e06e32f30e53ee4bbb4dbc7d50 to your computer and use it in GitHub Desktop.
Save joeycato/7f1d78e06e32f30e53ee4bbb4dbc7d50 to your computer and use it in GitHub Desktop.
# 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