FFmpeg Watermark: Add Image and Text Overlays to Video

March 20, 2026 · RenderIO

The overlay filter is all you need

FFmpeg watermarking comes down to two filters: overlay for images, drawtext for text. Every other tutorial covers one or the other and stops. This guide goes further: responsive scaling, animated watermarks, subtitle burn-in, batch processing, and how to skip local FFmpeg entirely by doing it through an API.

If you just want the command:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "overlay=W-w-10:H-h-10" \
  output.mp4

That puts logo.png in the bottom-right corner with 10px padding. The rest of this page covers everything else you'll run into.

Image watermark basics

The overlay filter takes two inputs (a main video and an overlay image) and composites them together. The syntax is overlay=x:y where x and y are pixel coordinates from the top-left corner.

ffmpeg -i video.mp4 -i watermark.png \
  -filter_complex "overlay=10:10" \
  output.mp4

This places the watermark 10 pixels from the top-left. Straightforward, but hardcoded pixel values break when your videos have different resolutions.

FFmpeg gives you variables to handle this:

  • W or main_w — video width

  • H or main_h — video height

  • w or overlay_w — watermark width

  • h or overlay_h — watermark height

With these, you can position the watermark relative to the video size regardless of resolution.

Position cheat sheet

Top-left with padding:

overlay=10:10

Top-right:

overlay=W-w-10:10

Bottom-left:

overlay=10:H-h-10

Bottom-right (most common for logos):

overlay=W-w-10:H-h-10

Dead center:

overlay=(W-w)/2:(H-h)/2

The -10 in each expression adds 10 pixels of padding from the edge. Adjust to taste.

Scaling watermarks with scale2ref

Here's the problem nobody talks about: your watermark image is probably the wrong size for your video. A 500px-wide logo on a 640px video covers most of the frame. On a 4K video, the same logo becomes a tiny speck.

The scale2ref filter scales one input relative to another. It uses the video's dimensions as a reference to resize the watermark proportionally.

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.2[logo][video];[video][logo]overlay=W-w-10:H-h-10" \
  output.mp4

Breaking this down:

  • [1][0] — feed the watermark (input 1) and video (input 0) into scale2ref

  • oh*mdar — output width = output height × main display aspect ratio (preserves proportions)

  • ih*0.2 — output height = 20% of the video height

  • [logo][video] — label the outputs

  • [video][logo]overlay=... — overlay the scaled logo on the video

The 0.2 controls the watermark size relative to the video. Change it to 0.1 for 10% height or 0.3 for 30%. This is the piece most tutorials skip, and it's what makes your watermark look right on any resolution from 480p to 4K.

Watch out: the input order for scale2ref matters. [1][0] means "scale input 1 using input 0 as reference." Swap them and you'll scale the video instead of the watermark. The output will be a mess.

Controlling watermark opacity

A fully opaque watermark over your video content is aggressive. For stock footage previews or subtle branding, semi-transparent works better.

Use colorchannelmixer to control the alpha channel before the overlay:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "[1]format=rgba,colorchannelmixer=aa=0.3[logo];[logo][0]scale2ref=oh*mdar:ih*0.2[logo][video];[video][logo]overlay=(W-w)/2:(H-h)/2" \
  output.mp4

aa=0.3 sets 30% opacity. The watermark is visible but doesn't dominate the frame. Stock footage platforms typically use 0.3 to 0.5 for preview watermarks.

The filter order matters: format and opacity first, then scale, then overlay. Get the order wrong and you'll either get a No such filter error or the opacity won't apply. If your watermark PNG doesn't have an alpha channel already, the format=rgba step is required. Without it, colorchannelmixer=aa has nothing to work with.

Text watermarks with drawtext

Sometimes you don't have a logo file. Or you need text that changes per video: a copyright notice, a username, a timestamp. The drawtext filter handles all of this.

ffmpeg -i video.mp4 \
  -vf "drawtext=text='© 2026 Your Company':fontcolor=white:fontsize=24:x=10:y=h-th-10" \
  output.mp4
  • text — the watermark text

  • fontcolor — text color (white, black, hex like 0xFFFFFF)

  • fontsize — size in pixels

  • x and y — position (same variables as overlay, plus tw/th for text width/height)

Text with a background box

White text on a bright video disappears. Add a semi-transparent background:

ffmpeg -i video.mp4 \
  -vf "drawtext=text='© 2026 Your Company':fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=h-th-10" \
  output.mp4

boxcolor=black@0.5 creates a 50% transparent black box behind the text. boxborderw=5 adds 5px padding inside the box.

Dynamic timestamps

For production review copies, burned-in timecode tells reviewers exactly where they are in the video:

ffmpeg -i video.mp4 \
  -vf "drawtext=text='%{pts\:hms}':fontcolor=white:fontsize=18:box=1:boxcolor=black@0.5:boxborderw=3:x=10:y=10" \
  output.mp4

%{pts\:hms} prints the current timestamp in hours:minutes:seconds format. The backslash before the colon escapes it within the filter expression. Skip it and FFmpeg will misparse the filter string.

Custom font

ffmpeg -i video.mp4 \
  -vf "drawtext=fontfile=/path/to/Roboto-Bold.ttf:text='PREVIEW':fontcolor=white@0.7:fontsize=48:x=(w-text_w)/2:y=(h-th)/2" \
  output.mp4

The fontfile parameter points to a .ttf or .otf file on your system. If you skip it, FFmpeg falls back to a default font that varies by platform. Fine for testing, unreliable for production. On Linux, /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf is usually a safe bet. On macOS, try /System/Library/Fonts/Helvetica.ttc.

Combining image and text watermarks

You can chain both in a single command using filter_complex:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.15[logo][video];[video][logo]overlay=W-w-10:10[bg];[bg]drawtext=text='© 2026 Your Company':fontcolor=white:fontsize=20:box=1:boxcolor=black@0.4:boxborderw=4:x=10:y=h-th-10" \
  output.mp4

This puts a scaled logo in the top-right and copyright text in the bottom-left. The filter chain matters: scale the logo, overlay it (labeling the result [bg]), then draw text on that intermediate result.

Animated and moving watermarks

Static watermarks are easy to crop or blur out. A watermark that moves across the frame is harder to remove, which is why TikTok and stock footage sites use this approach.

Scrolling watermark (left to right)

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.1[logo][video];[video][logo]overlay='if(lt(mod(t*100,W+w),W+w),mod(t*100,W+w)-w,W-mod(t*100,W+w))':H-h-10" \
  output.mp4

The mod(t*100,W+w) expression moves the watermark horizontally based on time. Adjust the 100 multiplier to change speed. Lower values move slower.

Position-shifting watermark (corner to corner)

This one jumps between corners every few seconds, similar to the DVD screensaver approach that stock footage sites use:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.12[logo][video];[video][logo]overlay=x='if(lt(mod(t,20),10),10,W-w-10)':y='if(lt(mod(t+5,20),10),10,H-h-10)'" \
  output.mp4

The watermark switches position on a 20-second cycle. The +5 offset on the y-axis means horizontal and vertical changes don't happen at the same time, so it hits all four corners.

Tiled/repeated watermark

For heavy-duty protection (preview reels, client review copies), tile the watermark across the entire frame:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "[1]format=rgba,colorchannelmixer=aa=0.15,scale=200:-1[wm];[wm]tile=4x4:overlap=0:init_padding=0[tiled];[0][tiled]overlay=0:0" \
  output.mp4

This creates a 4×4 grid of watermarks at 15% opacity. Impossible to crop out without destroying the video.

Burning in subtitles as a watermark

Subtitle burn-in bakes text directly into the video pixels. The viewer can't toggle it off or strip it. This is how most social media videos handle captions. Platform subtitle players are inconsistent, so creators burn them in.

From an SRT file

ffmpeg -i video.mp4 \
  -vf "subtitles=captions.srt:force_style='FontSize=22,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,Outline=2'" \
  output.mp4

The force_style parameter uses ASS format styling. PrimaryColour and OutlineColour are in &HAABBGGRR format (yes, BGR, not RGB). FFmpeg inherited this from the ASS/SSA spec. If your white text is coming out blue, you've got the byte order backwards.

From an ASS file with full styling

ffmpeg -i video.mp4 \
  -vf "ass=styled_captions.ass" \
  output.mp4

ASS files can include positioning, animation, fonts, and colors. If your subtitles are already styled, the ass filter preserves all of it.

Common gotcha: the subtitles and ass filters require FFmpeg compiled with libass. If you get No such filter: 'subtitles', your FFmpeg build doesn't include it. Check with ffmpeg -filters | grep subtitle. Most package managers install FFmpeg with libass, but minimal Docker images and static builds sometimes skip it.

For more on subtitle handling in automated workflows, see the n8n video processing guide.

Quality preservation

Re-encoding to add a watermark means your video goes through compression again. Every re-encode loses a little quality. Here's how to limit the damage.

Copy the audio stream

Audio doesn't change when you add a visual watermark. Copy it directly:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "overlay=W-w-10:H-h-10" \
  -c:a copy \
  output.mp4

-c:a copy passes the audio through without re-encoding. Faster processing, zero audio quality loss.

Use a low CRF value

CRF (Constant Rate Factor) controls the quality-to-filesize tradeoff. Lower values = higher quality = larger files.

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "overlay=W-w-10:H-h-10" \
  -c:v libx264 -crf 18 -preset slow -c:a copy \
  output.mp4

CRF 18 is visually lossless for most content. The default is 23, which introduces noticeable artifacts on detailed scenes. For a deeper breakdown of CRF values and presets, the FFmpeg cheat sheet covers the full range.

Match the source codec

If your input is H.265 (HEVC), output as H.265 to avoid unnecessary codec conversion:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "overlay=W-w-10:H-h-10" \
  -c:v libx265 -crf 20 -c:a copy \
  output.mp4

You can check your source codec with ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1 input.mp4.

Batch watermarking with a shell script

One video? Copy-paste a command. A hundred videos? You need a script.

#!/bin/bash
# watermark-batch.sh — watermark all MP4s in a directory
# Usage: ./watermark-batch.sh /path/to/videos logo.png

INPUT_DIR=$1
LOGO=$2
OUTPUT_DIR="${INPUT_DIR}/watermarked"
mkdir -p "$OUTPUT_DIR"

for video in "$INPUT_DIR"/*.mp4; do
  filename=$(basename "$video")
  echo "Watermarking: $filename"

  ffmpeg -i "$video" -i "$LOGO" \
    -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.2[logo][vid];[vid][logo]overlay=W-w-10:H-h-10" \
    -c:v libx264 -crf 20 -preset fast -c:a copy \
    "$OUTPUT_DIR/$filename" -y

done

echo "Done. Output in $OUTPUT_DIR"

This processes every .mp4 in the input directory, scales the logo to 20% of the video height, places it in the bottom-right, and saves the result to a watermarked/ subfolder. The -y flag overwrites existing files without prompting.

For a more advanced take on batch processing with random parameters to make each output unique, see batch make videos unique with FFmpeg.

Watermarking via API

Running FFmpeg locally works for one-off jobs. For production workloads, you want an API. No local FFmpeg to maintain, no server CPU getting crushed by encoding, and no dependency issues when you need a filter that wasn't compiled in.

If you haven't used an FFmpeg API before, the FFmpeg REST API tutorial walks through the basics in 5 minutes. The examples below assume you have an API key from RenderIO. You can grab one here if you haven't already.

curl

curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: ffsk_your_api_key" \
  -d '{
    "ffmpeg_command": "-i {{in_video}} -i {{in_logo}} -filter_complex \"[1][0]scale2ref=oh*mdar:ih*0.2[logo][vid];[vid][logo]overlay=W-w-10:H-h-10\" -c:v libx264 -crf 20 -c:a copy {{out_video}}",
    "input_files": {
      "in_video": "https://your-bucket.s3.amazonaws.com/input.mp4",
      "in_logo": "https://your-bucket.s3.amazonaws.com/logo.png"
    },
    "output_files": {
      "out_video": "watermarked.mp4"
    }
  }'

The API returns a command_id. Poll GET /api/v1/commands/:commandId until the status is SUCCESS, then grab the output from storage_url. The complete FFmpeg API guide covers the full lifecycle including webhook callbacks, so you don't need to poll at all.

Node.js

const response = await fetch("https://renderio.dev/api/v1/run-ffmpeg-command", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-KEY": "ffsk_your_api_key",
  },
  body: JSON.stringify({
    ffmpeg_command:
      '-i {{in_video}} -i {{in_logo}} -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.2[logo][vid];[vid][logo]overlay=W-w-10:H-h-10" -c:v libx264 -crf 20 -c:a copy {{out_video}}',
    input_files: {
      in_video: "https://your-bucket.s3.amazonaws.com/input.mp4",
      in_logo: "https://your-bucket.s3.amazonaws.com/logo.png",
    },
    output_files: { out_video: "watermarked.mp4" },
  }),
});

const { command_id } = await response.json();
console.log(`Submitted: ${command_id}`);

For polling, error handling, and batch patterns in Node.js, see the full Node.js integration guide.

Python

import requests

response = requests.post(
    "https://renderio.dev/api/v1/run-ffmpeg-command",
    headers={
        "Content-Type": "application/json",
        "X-API-KEY": "ffsk_your_api_key",
    },
    json={
        "ffmpeg_command": '-i {{in_video}} -i {{in_logo}} -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.2[logo][vid];[vid][logo]overlay=W-w-10:H-h-10" -c:v libx264 -crf 20 -c:a copy {{out_video}}',
        "input_files": {
            "in_video": "https://your-bucket.s3.amazonaws.com/input.mp4",
            "in_logo": "https://your-bucket.s3.amazonaws.com/logo.png",
        },
        "output_files": {"out_video": "watermarked.mp4"},
    },
)

command_id = response.json()["command_id"]
print(f"Submitted: {command_id}")

The Python integration guide covers the full polling loop and batch processing.

Batch watermarking via API

The real advantage of the API approach is parallelism. Instead of encoding videos one at a time on your machine, submit them all at once:

import requests
import concurrent.futures

videos = [
    "https://bucket.s3.amazonaws.com/video1.mp4",
    "https://bucket.s3.amazonaws.com/video2.mp4",
    "https://bucket.s3.amazonaws.com/video3.mp4",
    # ... hundreds more
]
logo = "https://bucket.s3.amazonaws.com/logo.png"

def watermark(video_url):
    resp = requests.post(
        "https://renderio.dev/api/v1/run-ffmpeg-command",
        headers={"Content-Type": "application/json", "X-API-KEY": "ffsk_your_key"},
        json={
            "ffmpeg_command": '-i {{in_video}} -i {{in_logo}} -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.2[logo][vid];[vid][logo]overlay=W-w-10:H-h-10" -c:v libx264 -crf 20 -c:a copy {{out_video}}',
            "input_files": {"in_video": video_url, "in_logo": logo},
            "output_files": {"out_video": f"watermarked_{video_url.split('/')[-1]}"},
        },
    )
    return resp.json()["command_id"]

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
    command_ids = list(pool.map(watermark, videos))

print(f"Submitted {len(command_ids)} watermark jobs")

Ten videos encoding in parallel instead of sequentially. No server resources consumed on your end. For the n8n no-code version of this workflow, see add watermarks to video in n8n.

Troubleshooting

A few errors you'll hit eventually:

"Input/output error" or blank output when overlay image has no alpha channel

If your watermark is a JPEG (no transparency), the overlay might not composite properly. Convert to PNG with transparency first, or force the format:

ffmpeg -i video.mp4 -i logo.jpg \
  -filter_complex "[1]format=rgba[logo];[0][logo]overlay=W-w-10:H-h-10" \
  output.mp4

"No such filter: 'subtitles'"

Your FFmpeg build wasn't compiled with libass. Check available filters: ffmpeg -filters 2>/dev/null | grep subtitle. On Ubuntu/Debian, install the full build: apt install ffmpeg (the default package usually includes libass). On Docker, use linuxserver/ffmpeg or jrottenberg/ffmpeg images which include it.

Watermark appears for one frame then disappears

This happens when the overlay input (your logo) is treated as a video with one frame. Add -loop 1 before the overlay input:

ffmpeg -i video.mp4 -loop 1 -i logo.png \
  -filter_complex "overlay=W-w-10:H-h-10:shortest=1" \
  output.mp4

The :shortest=1 tells FFmpeg to stop when the shortest input (the main video) ends.

Filter graph output pad "logo" not used

You labeled a stream [logo] in one part of the filter graph but spelled it differently in another, or forgot to use it. Filter graph labels are case-sensitive. Double-check that every labeled output has a matching input.

Encoding is painfully slow

Overlays require full re-encoding. Use -preset ultrafast during testing (quality suffers, but it's 5-10x faster). Switch to -preset slow or -preset medium for final output. Or offload the work to an FFmpeg API and let someone else's servers handle it.

Quick reference

All the FFmpeg watermark commands from this guide in one place.

Image overlay (bottom-right, 10px padding):

ffmpeg -i video.mp4 -i logo.png -filter_complex "overlay=W-w-10:H-h-10" -c:a copy output.mp4

Responsive scaling + overlay:

ffmpeg -i video.mp4 -i logo.png -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.2[logo][video];[video][logo]overlay=W-w-10:H-h-10" -c:a copy output.mp4

Semi-transparent image overlay:

ffmpeg -i video.mp4 -i logo.png -filter_complex "[1]format=rgba,colorchannelmixer=aa=0.3[logo];[logo][0]scale2ref=oh*mdar:ih*0.2[logo][video];[video][logo]overlay=(W-w)/2:(H-h)/2" -c:a copy output.mp4

Text watermark with background:

ffmpeg -i video.mp4 -vf "drawtext=text='© 2026 Company':fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=10:y=h-th-10" -c:a copy output.mp4

Burned-in subtitles:

ffmpeg -i video.mp4 -vf "subtitles=captions.srt" -c:a copy output.mp4

Image + text combo:

ffmpeg -i video.mp4 -i logo.png -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.15[logo][video];[video][logo]overlay=W-w-10:10[bg];[bg]drawtext=text='© 2026 Company':fontcolor=white:fontsize=20:box=1:boxcolor=black@0.4:boxborderw=4:x=10:y=h-th-10" -c:a copy output.mp4

Moving watermark:

ffmpeg -i video.mp4 -i logo.png -filter_complex "[1][0]scale2ref=oh*mdar:ih*0.12[logo][video];[video][logo]overlay=x='if(lt(mod(t,20),10),10,W-w-10)':y='if(lt(mod(t+5,20),10),10,H-h-10)'" -c:a copy output.mp4

For the full list of FFmpeg operations with API examples, see the commands reference. And if you want to skip FFmpeg entirely and use a hosted API, the complete FFmpeg API guide walks through the setup from scratch.

FAQ

Can I add a watermark without re-encoding the video?

No. Adding a watermark modifies the video frames, which requires decoding and re-encoding. You can minimize quality loss by using a low CRF value (18 is visually lossless) and copying the audio with -c:a copy. There's no way around the re-encode for the video stream itself.

What image format should my watermark be?

PNG with transparency. JPEGs don't support alpha channels, so your watermark will have an opaque rectangular background instead of blending into the video. If you're stuck with a JPEG, add format=rgba in the filter chain to at least avoid errors, but the result won't look as clean.

How do I watermark only part of the video (e.g., the first 10 seconds)?

Use the enable parameter on the overlay filter:

ffmpeg -i video.mp4 -i logo.png \
  -filter_complex "overlay=W-w-10:H-h-10:enable='between(t,0,10)'" \
  output.mp4

between(t,0,10) applies the overlay only when the timestamp is between 0 and 10 seconds.

Does watermarking affect video file size?

Yes, re-encoding always changes the file size. With the same CRF value, the watermarked areas add slightly more data because they increase visual complexity. In practice the size difference is under 5% for most content.

Can I use a GIF or video as a watermark instead of a static image?

Yes. Replace the static image input with a GIF or video file. FFmpeg treats any video input as a valid overlay source. For GIFs, add -ignore_loop 0 to loop the animation for the full duration of the main video.

How do I remove a watermark from a video?

FFmpeg has a delogo filter that can remove simple static watermarks by interpolating surrounding pixels: ffmpeg -i input.mp4 -vf "delogo=x=10:y=10:w=100:h=50" output.mp4. It works on small, solid-color logos in corners. For complex or moving watermarks, it won't produce clean results. This guide is about adding watermarks. The reverse is intentionally harder.