Learn · Developers

How to create a boomerang video with Python

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 speed takes an array of tweens, and a negative speed plays the source backwards.
  • The script builds one clip whose speed alternates between 1 and -1, once for each pass.
  • START and LENGTH choose the moment. LOOPS sets 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.

Python
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:

Text
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.mp4

This 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:

JSON
[
  {
    "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:

  • trim is where the boomerang starts in the source: 2.2 seconds into the skier clip.
  • speed is an array of tweens, one for each pass. [SPEED, -SPEED] * LOOPS alternates the direction. Each tween has the same from and to value, so the speed stays constant within a pass.
  • The clip’s length is 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:

ConstantDefaultWhat it sets
SOURCEThe skier clipThe video URL. It must be a URL the API can fetch, not a file on your computer.
START2.2The source second where the boomerang starts.
LENGTH1.2The seconds of source played in each direction.
SPEED1The playback rate. 2 plays the moment twice as fast.
LOOPS3The number of forward-and-back passes.
FPS25The 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.

PassSource frames shown, in order
First forward100, 101, 102 … 129
Backward130, 130, 129, 128 … 102
Next forward101, 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():

Python
"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.

Keep reading

Start free

  • Unlimited developer sandbox
  • No credit card required