Skip to content

Instantly share code, notes, and snippets.

@mjforan
Created January 7, 2022 17:24
Show Gist options
  • Save mjforan/7005699eea8f1014a1ad73d59f75caf3 to your computer and use it in GitHub Desktop.
Save mjforan/7005699eea8f1014a1ad73d59f75caf3 to your computer and use it in GitHub Desktop.
Create boomerang animation from video or frames
# Goal: video frames are duplicated and reversed (creating effect like Instagram's boomerang) and combined in animated webp
# Requires: ffmpeg, tested on ffmpeg 2021-11-10-git-44c65c6cc0-full_build-www.gyan.dev with Python 3.9.2 on Windows 10
# Args: mode input_file output_file [start_time] [end_time]
# Example: python boomerang.py v test.mp4 out.webp -s 00:00:02 -e 00:00:10
import argparse
import os
import shutil
from signal import signal, SIGINT
import subprocess
import sys
temp_modified = False
out_modified = False
dir_temp = "boomerang_temp"
parser = argparse.ArgumentParser(description='Convert video to boomerang animated webp')
parser.add_argument('mode', help='mode: v for video and f for frames')
parser.add_argument('input', help='input file or directory')
parser.add_argument('output', help='output file')
parser.add_argument('-s', dest='start', help='optional start time (only applicable to video mode)')
parser.add_argument('-e', dest='end', help='optional end time (only applicable to video mode)')
args = parser.parse_args()
# clean up if killed prematurely
def kill_handler(signal_received, frame):
print('killed')
# don't delete unless we passed the prompt about overwriting contents, in case the user had important files there
if temp_modified:
shutil.rmtree(dir_temp)
if out_modified:
os.remove(args.output)
sys.exit(0)
signal(SIGINT, kill_handler)
# check if output file exists
if os.path.isfile(args.output):
delete = input("Output file exists. Overwrite? [y/N] ")
if delete.lower() == 'y' or delete.lower() == 'yes':
os.remove(args.output)
else:
sys.exit(-1)
# set flag so output file is cleaned up if the program is killed after this point
out_modified = True
# check if temp dir exists
if os.path.isdir(dir_temp):
if len(os.listdir(dir_temp))>0:
delete = input("Temp directory exists and is not empty. Overwrite contents? [y/N] ")
if delete.lower() == 'y' or delete.lower() == 'yes':
shutil.rmtree(dir_temp)
os.makedirs(dir_temp)
else:
sys.exit(-1)
else:
os.makedirs(dir_temp)
# set flag so temp is cleaned up if the program is killed after this point
temp_modified = True
if args.mode=="v":
# Extract frames from input video
start = ['-ss', args.start] if args.start else []
end = ['-to', args.end] if args.end else []
subprocess.run(['ffmpeg', '-i', args.input]+start+end+['{}/%08d.png'.format(dir_temp)])
else:
# copy frames to temp dir
shutil.rmtree(dir_temp)
shutil.copytree(args.input, dir_temp)
file_names = os.listdir(dir_temp)
indices = [int(name[:8].lstrip("0")) for name in file_names]
start = max(indices) # index of last frame, i.e. middle of boomerang
# mirror all frames about the last frame
for i in range(len(indices)):
frm = start-i # work backwards from middle point
to = start+1+i
shutil.copyfile(dir_temp+"\\"+"{:08d}.png".format(frm), dir_temp+"\\"+"{:08d}.png".format(to))
# you might want to change encoder quality for smaller file size and smoother playback
subprocess.run(['ffmpeg', '-start_number', str(min(indices)), '-i', dir_temp+'\%8d.png', '-lossless', '1', '-loop', '0', args.output])
# clean up temp dir
shutil.rmtree(dir_temp)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment