FFmpeg API with Python: Convert, Resize, and Process Video

March 9, 2026 · RenderIO

Python + FFmpeg without installing FFmpeg

You're building a Python application that needs to process video. Maybe it's a Django app handling uploads, or a data pipeline extracting frames, or a script that batch-converts a folder of MOV files.

The usual approach is installing FFmpeg locally and calling it with subprocess.run(). That works until you deploy to a platform that doesn't have FFmpeg, or the binary is compiled without the codec you need, or your Docker image bloats to 800MB. If you've been through the self-hosted vs hosted FFmpeg debate, you know the pain.

The API approach is simpler: requests.post() with a JSON body. The only dependency is requests.

Configuration

import requests
import time

API_KEY = "ffsk_your_api_key"
BASE_URL = "https://renderio.dev/api/v1"

HEADERS = {
    "Content-Type": "application/json",
    "X-API-KEY": API_KEY
}

def run_ffmpeg(command, input_files, output_files):
    """Submit an FFmpeg command and wait for the result."""
    # Submit
    response = requests.post(
        f"{BASE_URL}/run-ffmpeg-command",
        headers=HEADERS,
        json={
            "ffmpeg_command": command,
            "input_files": input_files,
            "output_files": output_files
        }
    )
    response.raise_for_status()
    data = response.json()
    command_id = data["command_id"]

    # Poll
    while True:
        status = requests.get(
            f"{BASE_URL}/commands/{command_id}",
            headers=HEADERS
        ).json()

        if status["status"] == "completed":
            return status["output_files"]
        elif status["status"] == "failed":
            raise Exception(f"FFmpeg failed: {status.get('error', 'Unknown error')}")

        time.sleep(2)

This helper function handles submission and polling. Every example below uses it.

Convert video formats

MOV to MP4 is the most common conversion:

result = run_ffmpeg(
    command="-i {input} -c:v libx264 -preset fast -crf 22 -c:a aac -b:a 128k {output}",
    input_files={"input": "https://example.com/video.mov"},
    output_files={"output": "converted.mp4"}
)
print(f"Download: {result['output']}")

MP4 to WebM for web playback:

result = run_ffmpeg(
    command="-i {input} -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus {output}",
    input_files={"input": "https://example.com/video.mp4"},
    output_files={"output": "web-ready.webm"}
)

Any format to any format. The FFmpeg command is identical to what you'd run locally — if you already know FFmpeg commands, you already know the API.

Resize video

Scale to 720p while maintaining aspect ratio:

result = run_ffmpeg(
    command="-i {input} -vf scale=-2:720 -c:v libx264 -crf 23 -c:a copy {output}",
    input_files={"input": "https://example.com/4k-video.mp4"},
    output_files={"output": "720p.mp4"}
)

The -2 in scale=-2:720 means "calculate width automatically while keeping it divisible by 2." FFmpeg requires even dimensions for H.264.

Resize for Instagram (1080x1080 square with padding):

result = run_ffmpeg(
    command="-i {input} -vf \"scale=1080:1080:force_original_aspect_ratio=decrease,pad=1080:1080:(ow-iw)/2:(oh-ih)/2:black\" -c:v libx264 -crf 23 -c:a aac {output}",
    input_files={"input": "https://example.com/video.mp4"},
    output_files={"output": "instagram-square.mp4"}
)

Resize for TikTok (1080x1920 vertical):

result = run_ffmpeg(
    command="-i {input} -vf \"scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black\" -c:v libx264 -crf 23 -c:a aac {output}",
    input_files={"input": "https://example.com/video.mp4"},
    output_files={"output": "tiktok-vertical.mp4"}
)

Trim and cut video

For a full walkthrough of keyframe-accurate cutting, input vs output seeking, and batch trimming, see the FFmpeg trim video guide.

Extract a 30-second clip starting at 1 minute:

result = run_ffmpeg(
    command="-i {input} -ss 00:01:00 -t 00:00:30 -c copy {output}",
    input_files={"input": "https://example.com/long-video.mp4"},
    output_files={"output": "clip.mp4"}
)

Using -c copy makes this nearly instant because it copies without re-encoding. If you need frame-accurate trimming, re-encode:

result = run_ffmpeg(
    command="-i {input} -ss 00:01:00 -t 00:00:30 -c:v libx264 -crf 23 -c:a aac {output}",
    input_files={"input": "https://example.com/long-video.mp4"},
    output_files={"output": "precise-clip.mp4"}
)

Extract audio

MP4 to MP3:

result = run_ffmpeg(
    command="-i {input} -vn -acodec libmp3lame -q:a 2 {output}",
    input_files={"input": "https://example.com/video.mp4"},
    output_files={"output": "audio.mp3"}
)

Extract as WAV (lossless):

result = run_ffmpeg(
    command="-i {input} -vn -acodec pcm_s16le -ar 44100 {output}",
    input_files={"input": "https://example.com/video.mp4"},
    output_files={"output": "audio.wav"}
)

Add a watermark

Overlay a PNG logo in the bottom-right corner:

result = run_ffmpeg(
    command="-i {video} -i {logo} -filter_complex \"overlay=W-w-10:H-h-10\" {output}",
    input_files={
        "video": "https://example.com/video.mp4",
        "logo": "https://example.com/logo.png"
    },
    output_files={"output": "watermarked.mp4"}
)

W-w-10 means "main video width minus logo width minus 10px padding." Same for height. For responsive scaling, opacity control, text watermarks, and animated overlays, see the FFmpeg watermark guide.

Generate thumbnails

For advanced frame extraction — scene detection, keyframe-only mode, and batch processing across hundreds of videos — see the FFmpeg frame extraction guide.

Extract a single frame at the 5-second mark:

result = run_ffmpeg(
    command="-i {input} -ss 00:00:05 -vframes 1 -q:v 2 {output}",
    input_files={"input": "https://example.com/video.mp4"},
    output_files={"output": "thumbnail.jpg"}
)

Batch processing

Process multiple videos concurrently using ThreadPoolExecutor:

from concurrent.futures import ThreadPoolExecutor, as_completed

videos = [
    ("https://example.com/video1.mp4", "output1.mp4"),
    ("https://example.com/video2.mp4", "output2.mp4"),
    ("https://example.com/video3.mp4", "output3.mp4"),
]

def process_one(input_url, output_name):
    return run_ffmpeg(
        command="-i {input} -c:v libx264 -crf 23 -c:a aac {output}",
        input_files={"input": input_url},
        output_files={"output": output_name}
    )

with ThreadPoolExecutor(max_workers=10) as executor:
    futures = {
        executor.submit(process_one, url, name): name
        for url, name in videos
    }
    for future in as_completed(futures):
        name = futures[future]
        try:
            result = future.result()
            print(f"{name}: {result['output']}")
        except Exception as e:
            print(f"{name}: Failed - {e}")

10 concurrent workers means 10 videos processing at once. Each runs in its own isolated container, so there's no resource contention between jobs. This is particularly useful for e-commerce video automation where you might need to process hundreds of product videos in a single run.

Error handling

Build robust error handling around the helper:

def run_ffmpeg_safe(command, input_files, output_files, timeout=300):
    """Submit FFmpeg command with timeout and error handling."""
    try:
        response = requests.post(
            f"{BASE_URL}/run-ffmpeg-command",
            headers=HEADERS,
            json={
                "ffmpeg_command": command,
                "input_files": input_files,
                "output_files": output_files
            },
            timeout=10
        )
        response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 401:
            raise Exception("Invalid API key")
        elif e.response.status_code == 400:
            raise Exception(f"Bad request: {e.response.json()}")
        raise

    command_id = response.json()["command_id"]
    start = time.time()

    while time.time() - start < timeout:
        status = requests.get(
            f"{BASE_URL}/commands/{command_id}",
            headers=HEADERS,
            timeout=10
        ).json()

        if status["status"] == "completed":
            return status["output_files"]
        elif status["status"] == "failed":
            raise Exception(f"FFmpeg error: {status.get('error')}")

        time.sleep(2)

    raise TimeoutError(f"Command {command_id} did not complete within {timeout}s")

This handles API errors, FFmpeg failures, and timeouts. Use this version in production.

Django integration example

If you're using Django, here's a view that accepts a video URL and returns the processed result:

from django.http import JsonResponse
from django.views.decorators.http import require_POST
import json

@require_POST
def process_video(request):
    body = json.loads(request.body)
    video_url = body.get("video_url")
    operation = body.get("operation", "compress")

    commands = {
        "compress": "-i {input} -c:v libx264 -crf 28 -preset fast -c:a aac -b:a 96k {output}",
        "thumbnail": "-i {input} -ss 00:00:03 -vframes 1 -q:v 2 {output}",
        "tiktok": '-i {input} -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black" -c:v libx264 -crf 23 -c:a aac {output}',
    }

    cmd = commands.get(operation)
    if not cmd:
        return JsonResponse({"error": "Unknown operation"}, status=400)

    ext = "jpg" if operation == "thumbnail" else "mp4"

    try:
        result = run_ffmpeg_safe(
            command=cmd,
            input_files={"input": video_url},
            output_files={"output": f"processed.{ext}"}
        )
        return JsonResponse({"url": result["output"]})
    except Exception as e:
        return JsonResponse({"error": str(e)}, status=500)

This gives you a video processing endpoint in about 20 lines. No FFmpeg binary on your server.

FAQ

Do I need to install FFmpeg on my server?

No. The whole point of an API approach is that FFmpeg runs on RenderIO's infrastructure. Your Python code sends HTTP requests — it doesn't call any local binary.

What Python version do I need?

Python 3.7 or later. The code uses f-strings and requests. If you're on 3.10+, you could also use httpx with async support for better concurrency.

How long do output files stay available?

Output URLs are pre-signed and expire after a set period. Download the file or store it in your own S3/R2 bucket right after processing completes.

Can I process videos larger than 1GB?

Yes, but processing time increases with file size. For very large files, consider trimming first (which is near-instant with -c copy) and then processing the smaller clip. See the curl examples for trimming commands.

What happens if the FFmpeg command has a syntax error?

The API returns a 400 status with details about what went wrong. The run_ffmpeg_safe function above catches this and raises a readable exception.

Next steps

If you want the same setup in JavaScript, the Node.js tutorial covers fetch, webhooks, and Express integration. For a quick reference of common commands, the FFmpeg cheat sheet has 50 copy-paste examples.

The Starter plan at $9/mo includes 500 commands. Get your API key to start building.