Batch Make Videos Unique with FFmpeg

February 28, 2026 · RenderIO

Generating unique variations shouldn't be manual work

You have a video. You need 10 unique copies. Each one needs to be different enough that duplicate detection can't match them, but identical enough that viewers see the same content.

Doing this manually means running FFmpeg 10 times with different parameters. That's tedious and error-prone. A script does it in seconds.

This post covers three approaches: shell scripts (local), Python scripts (API), and n8n workflows (no code).

Approach 1: Shell script (local FFmpeg)

If you have FFmpeg installed locally, this bash script generates N variations:

#!/bin/bash
# make-unique.sh - Generate N unique video variations
# Usage: ./make-unique.sh source.mp4 10

SOURCE=$1
COUNT=${2:-5}
BASENAME=$(basename "$SOURCE" .mp4)

for i in $(seq 1 $COUNT); do
  CROP=$((2 + RANDOM % 8))
  BRIGHT=$(echo "scale=3; ($RANDOM % 40 - 20) / 1000" | bc)
  NOISE=$((3 + RANDOM % 5))
  PITCH=$(echo "scale=4; 1.0 + ($RANDOM % 20 - 10) / 2000" | bc)
  CRF=$((21 + RANDOM % 5))
  HUE=$((RANDOM % 6 - 3))

  OUTPUT="${BASENAME}_v${i}.mp4"

  echo "Generating variation $i/$COUNT: crop=$CROP bright=$BRIGHT noise=$NOISE pitch=$PITCH crf=$CRF hue=$HUE"

  ffmpeg -y -i "$SOURCE" \
    -vf "crop=iw-${CROP}:ih-${CROP}:${CROP}/2:${CROP}/2,eq=brightness=${BRIGHT},noise=alls=${NOISE}:allf=t,hue=h=${HUE}" \
    -af "asetrate=44100*${PITCH},aresample=44100" \
    -c:v libx264 -crf $CRF \
    -map_metadata -1 \
    "$OUTPUT" 2>/dev/null

  echo "  → $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
done

echo "Done. Generated $COUNT variations."

Run it:

chmod +x make-unique.sh
./make-unique.sh my-video.mp4 10

Output:

Generating variation 1/10: crop=6 bright=0.012 noise=5 pitch=1.0035 crf=23 hue=-1
  → my-video_v1.mp4 (4.2M)
Generating variation 2/10: crop=4 bright=-0.008 noise=7 pitch=0.9960 crf=22 hue=2
  → my-video_v2.mp4 (4.5M)
...
Done. Generated 10 variations.

Pros: Fast, no API costs, works offline. Cons: Uses your local CPU, doesn't scale, blocks your machine during encoding.

The same batch pattern works for other conversions too. If you need to convert a folder of MP4s to GIF, the approach is similar but with palette optimization in the filter chain.

Approach 2: Python script with API

Offload the encoding to RenderIO. Your machine stays free. Works from any environment.

import requests
import time
import random
import sys

API_KEY = "ffsk_your_api_key"
BASE_URL = "https://renderio.dev/api/v1"
HEADERS = {"Content-Type": "application/json", "X-API-KEY": API_KEY}

def generate_params(count):
    """Generate unique parameter sets for N variations."""
    params = []
    for i in range(count):
        params.append({
            "crop": random.randint(2, 10),
            "bright": round(random.uniform(-0.02, 0.02), 3),
            "noise": random.randint(3, 7),
            "pitch": round(random.uniform(0.994, 1.006), 4),
            "crf": random.randint(21, 25),
            "hue": random.randint(-3, 3),
            "sat": round(random.uniform(0.98, 1.02), 3),
        })
    return params

def build_command(p):
    """Build FFmpeg command string from parameters."""
    vf = (
        f'crop=iw-{p["crop"]}:ih-{p["crop"]}:{p["crop"]//2}:{p["crop"]//2},'
        f'eq=brightness={p["bright"]:.3f}:saturation={p["sat"]:.3f},'
        f'noise=alls={p["noise"]}:allf=t,'
        f'hue=h={p["hue"]}'
    )
    af = f'asetrate=44100*{p["pitch"]:.4f},aresample=44100'
    return f'-i {{in_video}} -vf "{vf}" -af "{af}" -c:v libx264 -crf {p["crf"]} -map_metadata -1 {{out_video}}'

def submit_job(source_url, output_name, params):
    """Submit one variation to the API."""
    cmd = build_command(params)
    res = requests.post(f"{BASE_URL}/run-ffmpeg-command", headers=HEADERS, json={
        "ffmpeg_command": cmd,
        "input_files": {"in_video": source_url},
        "output_files": {"out_video": output_name}
    })
    res.raise_for_status()
    return res.json()["command_id"]

def wait_for_jobs(command_ids):
    """Poll until all jobs complete."""
    results = {}
    pending = set(range(len(command_ids)))

    while pending:
        for i in list(pending):
            status = requests.get(
                f"{BASE_URL}/commands/{command_ids[i]}",
                headers=HEADERS
            ).json()

            if status["status"] == "completed":
                results[i] = status["output_files"]["out_video"]
                pending.discard(i)
                print(f"  Variation {i+1}: DONE → {results[i]}")
            elif status["status"] == "failed":
                results[i] = None
                pending.discard(i)
                print(f"  Variation {i+1}: FAILED → {status.get('error')}")

        if pending:
            time.sleep(3)

    return results

def main():
    source_url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com/source.mp4"
    count = int(sys.argv[2]) if len(sys.argv) > 2 else 5

    print(f"Generating {count} unique variations...")

    # Generate unique parameters
    all_params = generate_params(count)

    # Submit all jobs
    command_ids = []
    for i, params in enumerate(all_params):
        cmd_id = submit_job(source_url, f"variation_{i+1}.mp4", params)
        command_ids.append(cmd_id)
        print(f"  Submitted variation {i+1}: {cmd_id}")

    print(f"\nAll {count} jobs submitted. Waiting for completion...")

    # Wait for all to finish
    results = wait_for_jobs(command_ids)

    # Summary
    success = sum(1 for v in results.values() if v is not None)
    print(f"\nComplete: {success}/{count} variations generated successfully")

    for i in sorted(results.keys()):
        if results[i]:
            print(f"  variation_{i+1}.mp4: {results[i]}")

if __name__ == "__main__":
    main()

Run it:

python batch-unique.py "https://your-storage.com/source.mp4" 10

Output:

Generating 10 unique variations...
  Submitted variation 1: cmd_a1b2c3
  Submitted variation 2: cmd_d4e5f6
  ...
All 10 jobs submitted. Waiting for completion...
  Variation 3: DONE → https://media.renderio.dev/variation_3.mp4
  Variation 1: DONE → https://media.renderio.dev/variation_1.mp4
  ...
Complete: 10/10 variations generated successfully

All 10 encode simultaneously in isolated containers. Total time: same as encoding one video (they run in parallel).

Approach 3: n8n workflow (no code)

For non-developers, build it as an n8n workflow:

Node 1: Webhook Trigger

{ "videoUrl": "https://...", "count": 10 }

Node 2: Code node (generate parameters)

const count = $input.first().json.count || 5;
const videoUrl = $input.first().json.videoUrl;
const variations = [];

for (let i = 0; i < count; i++) {
  variations.push({
    json: {
      videoUrl,
      index: i + 1,
      crop: 2 + Math.floor(Math.random() * 8),
      bright: (Math.random() * 0.04 - 0.02).toFixed(3),
      noise: 3 + Math.floor(Math.random() * 5),
      pitch: (0.994 + Math.random() * 0.012).toFixed(4),
      crf: 21 + Math.floor(Math.random() * 5),
      hue: Math.floor(Math.random() * 7) - 3
    }
  });
}

return variations;

Node 3: Split in Batches (size: 5)

Node 4: HTTP Request (submit to RenderIO)

Reference {{ $json.crop }}, {{ $json.bright }}, etc. in the FFmpeg command.

Nodes 5-7: Poll and collect

Parameter randomization strategies

Fully random

Each parameter is independently random within a range. Simplest approach. Small chance of two similar combinations.

params = {
    "crop": random.randint(2, 10),
    "bright": round(random.uniform(-0.02, 0.02), 3),
    "noise": random.randint(3, 7),
}

Grid-based

Distribute parameters evenly across the range. Guarantees maximum diversity.

def grid_params(count):
    crops = [2, 4, 6, 8, 10]
    brights = [-0.02, -0.01, 0, 0.01, 0.02]

    params = []
    for i in range(count):
        params.append({
            "crop": crops[i % len(crops)],
            "bright": brights[i % len(brights)],
            "crf": 21 + (i % 5),
        })
    return params

Seeded random (deterministic)

Same seed always produces the same parameters. Useful when you need to reproduce variations.

random.seed(42)  # Same seed = same variations every time
params = generate_params(10)

Verifying uniqueness

After batch generation, verify each file is unique:

# Check file sizes (should all be different)
ls -la variation_*.mp4

# Check MD5 hashes (must all be different)
md5sum variation_*.mp4

If any two files have the same MD5 hash, the parameter randomization produced identical values. Re-run with wider ranges.

Scaling considerations

VariationsAPI Processing TimeApproach
1-51-3 minAny approach
5-202-5 minAPI (parallel)
20-503-8 minAPI (parallel)
50-1005-15 minAPI (batched parallel)
100+15+ minAPI (batched, monitor rate limits)

All variations process in parallel on the API. The main bottleneck at high volume is rate limiting. Batch your submissions to stay within limits.

Get started

Pick your approach:

  1. Shell script: For quick local generation

  2. Python + API: For reliable, scalable generation

  3. n8n workflow: For no-code automation

The Growth plan at 29/mocovers1,000commands.ScaletoBusiness(29/mo covers 1,000 commands. Scale to Business (99/mo, 20,000 commands) as volume grows. For TikTok-specific automation, the TikTok content automation pipeline in n8n builds on these techniques. Dropshippers can check the dropshipping video automation guide for a supplier-to-TikTok pipeline.