Learn · Developers

How to add cinematic bars to a video with Python

Contents

Cinematic bars are the black bands above and below the picture that make a video look like a feature film. Trailers and music videos often use them to give ordinary 16:9 footage a widescreen feel.

The bars only look right at the right height. Too thin and they read as a mistake. Too thick and they cut into the shot.

This article shows how to work out the height for any widescreen ratio and add the bars with a Python script and Shotstack’s video editing API. We measured the bars on every frame of the rendered video.

TL;DR

  • Cinematic bars turn a 16:9 frame into a wider picture, such as 2.39:1, while the file stays 16:9.
  • Each bar is round((H - W / R) / 2) pixels high, for a frame W by H and a target ratio R.
  • On a 1920x1080 video, 2.39:1 needs two 138-pixel bars and leaves a 1920x804 picture.
  • The script draws both bars as one SVG on a track above the video, so the footage isn’t scaled or moved.
  • In our render, the top and bottom 138 rows were black on all 192 frames.

What are cinematic bars?

The bars come from cinema. Wikipedia’s article on letterboxing describes it as “the practice of transferring film shot in a widescreen aspect ratio to standard-width video formats”. The result has “mattes of empty space above and below it”.

Films use a few standard shapes. Wikipedia’s article on aspect ratios says “the common film aspect ratios used in theaters are 1.85:1 and 2.39:1.”

You’ll also see 2.35:1. The article on the anamorphic format explains that 2.35:1 was redefined in 1971 as about 2.39:1, and that “generally 2.35, 2.39 and 2.40 mean the same to professionals”.

When you add bars to 16:9 footage, you aren’t fitting a wider film into the frame. You are covering the top and bottom of the picture to give it a wider shape. Keep anything important, such as faces and on-screen text, in the middle of the frame.

How tall should cinematic bars be?

For a frame W pixels wide and H pixels high, a picture with the ratio R is W / R pixels high. The bars share the rest of the height equally:

Text
bar height = round((H - W / R) / 2)

For 2.39:1 on a 1920x1080 frame, the picture is 1920 / 2.39 = 803.3 pixels high. That leaves 276.7 pixels, or 138.3 pixels for each bar, which rounds to 138. The picture between the bars is then 1920x804, a ratio of 2.388:1. Video can’t show part of a pixel, so that is the closest you can get.

Target ratioBars on 1920x1080PictureBars on 1280x720Picture
1.85:121 px each1920x103814 px each1280x692
2.35:1131 px each1920x81888 px each1280x544
2.39:1138 px each1920x80492 px each1280x536

A ratio only needs bars if it is wider than the frame. A 16:9 frame is already 1.78:1, so a target of 1.78:1 or less needs no bars. The script below stops with a message in that case.

Add cinematic bars to a video with Python

The script adds 2.39:1 bars to an 8-second clip of Shotstack’s stock road footage. It uses only the Python standard library. Set SHOTSTACK_API_KEY to the API key from your Shotstack dashboard, then run it with Python 3. Each render uses credits from your plan, as listed on the pricing page.

Python
import json
import os
import sys
import time
import urllib.error
import urllib.request

API = "https://api.shotstack.io/edit/v1/render"
VIDEO = "https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/footage/road.mp4"
WIDTH, HEIGHT = 1920, 1080
RATIO = 2.39  # try 2.35 or 1.85
START, LENGTH = 2, 8  # seconds of the source to use


def bar_height(width, height, ratio):
    """Pixels in each bar, so the picture between the bars is width:ratio."""
    if ratio <= width / height:
        sys.exit(f"{ratio}:1 is not wider than {width}x{height}, so no bars are needed")
    return round((height - width / ratio) / 2)


def bars_svg(width, height, bar):
    """A transparent frame with a black bar at the top and the bottom."""
    return (
        f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" '
        f'width="{width}" height="{height}">'
        f'<rect x="0" y="0" width="{width}" height="{bar}" fill="#000"/>'
        f'<rect x="0" y="{height - bar}" width="{width}" height="{bar}" fill="#000"/>'
        "</svg>"
    )


def build_edit(bar):
    video = {"type": "video", "src": VIDEO, "trim": START, "volume": 0}
    bars = {"type": "svg", "src": bars_svg(WIDTH, HEIGHT, bar)}
    return {
        "timeline": {
            "tracks": [
                {"clips": [{"asset": bars, "start": 0, "length": LENGTH}]},
                {"clips": [{"asset": video, "start": 0, "length": LENGTH}]},
            ]
        },
        "output": {
            "format": "mp4",
            "size": {"width": WIDTH, "height": HEIGHT},
            "fps": 24,
        },
    }


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}: {' '.join(error.read().decode().split())[:300]}")


if __name__ == "__main__":
    bar = bar_height(WIDTH, HEIGHT, RATIO)
    picture = HEIGHT - 2 * bar
    print(
        f"{RATIO}:1 on {WIDTH}x{HEIGHT}: two {bar} px bars, picture {WIDTH}x{picture}"
    )
    edit = build_edit(bar)
    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.")
    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 cinematic_bars.py. It prints the bar size, the render ID, then the URL of the video:

Text
2.39:1 on 1920x1080: two 138 px bars, picture 1920x804
Render queued: 327c4382-f1da-43bc-84f9-d0c8e12a1b41
https://shotstack-api-v1-output.s3-ap-southeast-2.amazonaws.com/35tqpmb0ya/327c4382-f1da-43bc-84f9-d0c8e12a1b41.mp4

This is the rendered video:

Run python cinematic_bars.py --dry-run to print the bar size and the edit without rendering. This is the clip that holds the bars:

JSON
{
  "asset": {
    "type": "svg",
    "src": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1920 1080\" width=\"1920\" height=\"1080\"><rect x=\"0\" y=\"0\" width=\"1920\" height=\"138\" fill=\"#000\"/><rect x=\"0\" y=\"942\" width=\"1920\" height=\"138\" fill=\"#000\"/></svg>"
  },
  "start": 0,
  "length": 8
}

How the script works

  • bar_height() applies the formula above. It stops the script when the target ratio isn’t wider than the frame.
  • bars_svg() builds a transparent SVG the size of the frame, with one black rectangle at the top and one at the bottom. The bottom one starts at HEIGHT - bar, so it ends exactly at the bottom edge.
  • build_edit() puts the bars on the first track and the video on the second. The first track is drawn on top of the tracks below it, so the bars cover the footage. trim skips the first 2 seconds of the source, and volume is 0.
  • The output is 1920x1080 at 24 fps, the same size as the SVG, so every rectangle lands on the pixel it names.

The bars use the Edit API’s SVG asset, which takes raw SVG markup. The SVG guide notes that the outer element needs xmlns, a viewBox, and width and height attributes. Use it to draw shapes like these rectangles.

What the rendered video shows

We read every frame of the production render and found where the bars start and stop.

CheckResult
Size, frame rate, length1920x1080, 24 fps, 8.0 seconds, 192 frames
Top barRows 0 to 137 black on all 192 frames
Bottom barRows 942 to 1079 black on all 192 frames
Picture between the bars1920x804, a ratio of 2.388:1

We counted a row as black when its average brightness was 17 or lower on the 0 to 255 scale, where video black is 16. The brightest single pixel inside the bars was 35, at the edge where the bar meets the picture. Video compression softens hard edges like this one slightly.

Make a true widescreen file instead

The bars keep the file at 16:9, which suits players and platforms that expect 16:9 video. If you need a file that is itself 2.39:1, leave out the bars and set the output size to the picture size. In the script, replace the line edit = build_edit(bar) with:

Python
edit = build_edit(0)
edit["timeline"]["tracks"] = edit["timeline"]["tracks"][1:]  # the video track only
edit["output"]["size"] = {"width": 1920, "height": 804}

Our render of this edit was a 1920x804 file with square pixels, 192 frames long. It showed the same part of the footage as the picture between the bars, with a structural similarity (SSIM) score of 0.984 against it.

A 16:9 player shows a file like this with its own bars. Our guide to cropping and resizing videos covers custom output sizes in more detail.

Tips for cinematic bars

  • Frame for the bars. At 2.39:1 on 1080p footage, the bars cover 138 rows at the top and 138 at the bottom. Check that heads, horizons and captions sit in the middle 804 rows.
  • Pick the ratio for the mood. 2.39:1 gives the strongest widescreen look. 1.85:1 adds thin bars of 21 pixels each on 1080p.
  • Match the frame size. Change WIDTH and HEIGHT together with the output size. The SVG and the output must stay the same size for the bars to land on exact rows.
  • Put titles above the bars. A title clip on a track above the bars track is drawn on top of them. Our guide to positioning clips shows how to place it.
  • Going vertical? 16:9 footage in a 9:16 frame gets bars at the sides instead. Our guide to blurring the sides of a vertical video fills them with a blurred copy of the footage.

Add cinematic bars to your videos with Shotstack

You now have a script that works out the bar height for any widescreen ratio and renders the bars over your footage. Change RATIO to 2.35 or 1.85, or point VIDEO at your own clip, and run it again.

Create a free Shotstack account to get your API key and render your first widescreen video.

Frequently asked questions (FAQs)

How big are the bars on a 4K video?

The same formula works at any size. On a 3840x2160 frame, 2.39:1 bars are 277 pixels each and leave a 3840x1606 picture.

Can I add bars to only part of a video?

Yes. The bars are one clip, so give it its own start and length on the timeline. For example, a bars clip with start set to 5 and length set to 3 covers seconds 5 to 8 of the video and leaves the rest full frame.

Do cinematic bars lower the video quality?

No. The file keeps its full 1920x1080 size and the footage isn’t scaled. The bars cover the top and bottom rows of the picture, so that part of the shot isn’t shown.

Can I animate the bars in?

Yes, as one piece. Both bars are in one SVG clip, so a transition or animation such as a fade applies to both at once. The SVG guide says to animate an SVG asset as a whole with clip animations. To move the top and bottom bars separately, give each bar its own SVG clip.

Keep reading

Start free

  • Unlimited developer sandbox
  • No credit card required