Contents
A boomerang video plays a short moment forward, then backwards, over and over: a splash rises and falls back into the water, or a skier throws up snow and takes it back. It is a familiar look in short-form video.
Phone apps make one boomerang at a time. The usual way to make them from code is to reverse the clip with FFmpeg and join the two halves.
This article gives you a Python script that needs neither step. It builds the whole boomerang as one clip and renders it with Shotstack’s video editing API. We checked the result frame by frame on rendered files.
TL;DR
- A video asset’s
speedtakes an array of tweens, and a negative speed plays the source backwards. - The script builds one clip whose speed alternates between
1and-1, once for each pass. STARTandLENGTHchoose the moment.LOOPSsets how many times it plays forward and back.- In our test render, the frames played forward in order, then backwards in the same order, with no frame skipped.
- For a GIF, one loop is enough, because the GIF repeats itself.
What the script creates
With its defaults, the script takes 1.2 seconds of a skier carving a turn. It plays the moment forward, then backwards, three times. The result is a silent 7.2 second MP4.
Python builds the edit with the standard library only. The Edit API does the playback, so there is no FFmpeg on your machine and no file to upload when the footage is already online.
Create a boomerang video with Python
Set SHOTSTACK_API_KEY to the API key from your Shotstack dashboard, then run the script with Python 3. If you are new to the API, our guide to rendering your first video covers keys and render statuses.
import json
import os
import sys
import time
import urllib.error
import urllib.request
API = "https://api.shotstack.io/edit/v1/render"
SOURCE = "https://shotstack-assets.s3.ap-southeast-2.amazonaws.com/footage/skier.mp4"
START = 2.2 # source second where the boomerang starts
LENGTH = 1.2 # seconds of source to play forward, then backwards
SPEED = 1 # 2 plays the moment twice as fast
LOOPS = 3 # forward-and-back passes
FPS = 25 # output frame rate, the same as the source
def build_edit():
frames = round(LENGTH / SPEED * FPS) # whole frames in each direction
leg = frames / FPS
tweens = [
{"from": speed, "to": speed, "start": round(i * leg, 3), "length": leg}
for i, speed in enumerate([SPEED, -SPEED] * LOOPS)
]
asset = {
"type": "video",
"src": SOURCE,
"trim": START,
"volume": 0,
"speed": tweens,
}
clip = {"asset": asset, "start": 0, "length": round(2 * leg * LOOPS, 3)}
return {
"timeline": {"tracks": [{"clips": [clip]}]},
"output": {"format": "mp4", "resolution": "hd", "fps": FPS},
}
def call(url, body=None):
data = json.dumps(body).encode() if body else None
headers = {
"x-api-key": os.environ["SHOTSTACK_API_KEY"],
"Content-Type": "application/json",
}
try:
request = urllib.request.Request(url, data, headers)
with urllib.request.urlopen(request) as response:
return json.load(response)["response"]
except urllib.error.HTTPError as error:
sys.exit(f"HTTP {error.code}: {error.read().decode()[:300]}")
if __name__ == "__main__":
edit = build_edit()
if "--dry-run" in sys.argv:
print(json.dumps(edit, indent=2))
sys.exit()
if not os.environ.get("SHOTSTACK_API_KEY"):
sys.exit("Set SHOTSTACK_API_KEY first.")
length = edit["timeline"]["tracks"][0]["clips"][0]["length"]
print(f"{LOOPS} loops of {LENGTH}s at {SPEED}x: {length}s")
render_id = call(API, edit)["id"]
print("Render queued:", render_id)
while True:
time.sleep(5)
status = call(f"{API}/{render_id}")
if status["status"] == "done":
print(status["url"])
break
if status["status"] == "failed":
sys.exit(f"Render failed: {status.get('error')}")Run python boomerang.py. It prints the plan, then the URL of the rendered video:
3 loops of 1.2s at 1x: 7.2s
Render queued: 4ff32a76-4e1f-4065-aad4-3c14b94a2d33
https://shotstack-api-v1-output.s3-ap-southeast-2.amazonaws.com/35tqpmb0ya/4ff32a76-4e1f-4065-aad4-3c14b94a2d33.mp4This is the rendered video. The snow sprays out as the skier turns, then flows back under the skis:
Run python boomerang.py --dry-run to print the edit without rendering. It needs no API key. This is the speed array it builds:
[
{
"from": 1,
"to": 1,
"start": 0.0,
"length": 1.2
},
{
"from": -1,
"to": -1,
"start": 1.2,
"length": 1.2
},
{
"from": 1,
"to": 1,
"start": 2.4,
"length": 1.2
},
{
"from": -1,
"to": -1,
"start": 3.6,
"length": 1.2
},
{
"from": 1,
"to": 1,
"start": 4.8,
"length": 1.2
},
{
"from": -1,
"to": -1,
"start": 6.0,
"length": 1.2
}
]How one clip plays forward and backwards
build_edit() puts every pass in one video clip. Three settings do the work:
trimis where the boomerang starts in the source: 2.2 seconds into the skier clip.speedis an array of tweens, one for each pass.[SPEED, -SPEED] * LOOPSalternates the direction. Each tween has the samefromandtovalue, so the speed stays constant within a pass.- The clip’s
lengthis set by the script. Speed tweens don’t change a clip’s length, so it is two passes for each loop.
The animations guide documents the rule the script relies on: “a negative speed plays the video backwards.” Our guide to speed ramping a video with Python uses the same tween array to ease into slow motion.
The script also rounds each pass to a whole number of frames. At 25 fps, 1.2 seconds is 30 frames, so every pass has the same frame count.
Choose the moment, speed and loops
Change the constants at the top of the script:
| Constant | Default | What it sets |
|---|---|---|
SOURCE | The skier clip | The video URL. It must be a URL the API can fetch, not a file on your computer. |
START | 2.2 | The source second where the boomerang starts. |
LENGTH | 1.2 | The seconds of source played in each direction. |
SPEED | 1 | The playback rate. 2 plays the moment twice as fast. |
LOOPS | 3 | The number of forward-and-back passes. |
FPS | 25 | The output frame rate. Match it to the source. |
Short moments work best. One or two seconds with a clear action, such as a jump, a splash or a turn, reads well in both directions.
The clip is silent because volume is 0. Sound follows the speed, so it would play backwards too. Add music on a second track if you want sound.
What the rendered frames show
To check the frame order, we rendered the script’s edit with a test clip whose frames carry their own number. We then read the number back from every output frame. The test clip is 25 fps and the boomerang starts on frame 100.
| Pass | Source frames shown, in order |
|---|---|
| First forward | 100, 101, 102 … 129 |
| Backward | 130, 130, 129, 128 … 102 |
| Next forward | 101, 101, 102 … 129 |
- No frame was skipped or shown out of order in any pass.
- The frame at each turn stays on screen for two frames, or 0.08 seconds. It gives the turn a slight pause.
- All three backward passes were identical, and so were the forward passes after the first.
- At
SPEED = 2, each pass used every second frame and took 0.6 seconds. Three loops made a 3.6 second video.
Make a boomerang GIF
A GIF loops by itself, so it only needs one pass in each direction. Set LOOPS = 1 and change the output in build_edit():
"output": {"format": "gif", "resolution": "mobile", "fps": FPS, "repeat": True},In our test, the GIF had 60 frames in the same order as the MP4, and it repeats without end. GIF files are large, so keep them small on screen: the skier boomerang was 7.5 MB at mobile size (640 by 360) and 23 MB at hd.
Our guide to converting MP4 to GIF with Python covers the GIF output settings.
Loop your best moments with Shotstack
You now have a script that turns any moment of a video into a boomerang. It picks the moment with trim, alternates the speed to play it forward and backwards, and renders the result with the Edit API. Point it at your own footage and change three numbers to make a new one.
Create a free Shotstack account to get your API key and render your first boomerang video.
Frequently asked questions (FAQs)
Can I use a video file from my computer?
Yes. Upload it first, to your own storage or to Shotstack with the Ingest API, and set SOURCE to the URL it returns.
How do I find the right start time?
Scrub the video in any player to find the second where the action begins, then set START a little before it. For an exact frame, our guide to extracting frames from a video shows how to pull stills at chosen times.
Can I make the boomerang vertical for Reels or TikTok?
Yes, add "aspectRatio": "9:16" to the output. The clip fills the frame by default, so a landscape source is cropped at the sides. Choose a moment where the action stays near the center.