# How to add a watermark with FFmpeg

Canonical: https://rendobar.com/blog/ffmpeg-watermark-video/
Author: Abdelrahman Essawy
Published: 2026-08-20
Updated: 2026-08-20

---

## Key takeaways

- An image watermark needs -filter_complex, never -vf, because overlay takes two inputs and -vf only ever sees the first.
- 21 of the 40 overlay jobs in our archive failed, and 14 of those returned the same generic Invalid argument message with no detail.
- A text watermark is the opposite: drawtext takes one input, so -vf is correct and 15 of 18 drawtext jobs completed.
- Corner placement is arithmetic, not guesswork. W and H are the base video, w and h the overlay, so W-w-20 pins it 20px from the right edge.
- Median cost of an overlay job was $0.0027, which is roughly the median of every FFmpeg job in the archive. Watermarking is not an expensive operation.

## Short version

An image watermark, bottom right, 20 pixels from each edge:

```bash
ffmpeg -i base.mp4 -i logo.png \
  -filter_complex "[0:v][1:v]overlay=W-w-20:H-h-20" \
  -c:a copy out.mp4
```

A text watermark, centred at the bottom:

```bash
ffmpeg -i base.mp4 \
  -vf "drawtext=text='Rendobar':fontsize=36:fontcolor=white:x=(w-text_w)/2:y=h-th-60" \
  -c:a copy out.mp4
```

The difference between those two lines is the thing that breaks most attempts, and it is not stylistic. **The image version uses `-filter_complex`. The text version uses `-vf`.** Swapping them does not work.

## Why 21 of 40 overlay jobs failed

We went through every overlay job in Rendobar's history. **40 attempts, 19 completed, 21 failed.** Of the failures, **14 returned exactly this**:

```
Invalid argument. Check your FFmpeg flags and values.
```

Nothing in that message points at the cause, and every one of the 14 failed at **0 seconds**, meaning FFmpeg never got as far as reading the video.

The cause is structural. `overlay` composites **two** video streams. `-vf` builds a simple filter graph that only has access to the **first** input. So this is unfixable no matter what you put after the equals sign:

```bash
# Cannot work. -vf never sees logo.png.
ffmpeg -i base.mp4 -i logo.png -vf "overlay=10:10" out.mp4
```

`-filter_complex` is the version that can reference multiple inputs, and it needs labels to say which:

```bash
ffmpeg -i base.mp4 -i logo.png \
  -filter_complex "[0:v][1:v]overlay=10:10" \
  -c:a copy out.mp4
```

`[0:v]` is the video stream of input 0, `[1:v]` the video stream of input 1. The rule generalises: **any filter taking more than one input needs `-filter_complex` and explicit labels.** That includes `concat`, `blend`, `hstack` and every transition.

Text is the counterexample that proves it. `drawtext` generates its own pixels and needs only the one video, so `-vf` is correct and **15 of 18 drawtext jobs in the archive completed**.

## The four corners

Placement uses variables rather than fixed pixel values, which keeps a command working across resolutions. `W` and `H` are the **main** video's dimensions, `w` and `h` are the **overlay's**.

| Position | Expression |
|---|---|
| Top left | `overlay=20:20` |
| Top right | `overlay=W-w-20:20` |
| Bottom left | `overlay=20:H-h-20` |
| Bottom right | `overlay=W-w-20:H-h-20` |
| Centred | `overlay=(W-w)/2:(H-h)/2` |

Every one of those was run as a real job. The bottom-right and centred forms are the two that appear most in the archive.

Change the `20` to a percentage of the frame if you need the margin to scale: `overlay=W-w-W*0.02:H-h-H*0.02` keeps a 2% inset at any resolution.

## Sizing the watermark to the video

A fixed-size logo is wrong on most footage. Scale it relative to the base first, then composite:

```bash
ffmpeg -i base.mp4 -i logo.png \
  -filter_complex "[1:v]scale=iw*0.15:-1[wm];[0:v][wm]overlay=W-w-20:H-h-20" \
  -c:a copy out.mp4
```

That chain scales the logo to 15% of its own width with `-1` preserving aspect, names the result `[wm]`, and feeds it to the overlay. Two things there are easy to get wrong. **Every label you define must be consumed**, or the graph fails to build. And `-1` for height can produce an odd number, which some encoders reject, so `-2` is the safer choice when the output feeds H.264.

## Transparency

A PNG with an alpha channel composites correctly with no extra work. To make an opaque image semi-transparent, multiply its alpha:

```bash
ffmpeg -i base.mp4 -i logo.png \
  -filter_complex "[1:v]format=rgba,colorchannelmixer=aa=0.5[wm];[0:v][wm]overlay=W-w-20:H-h-20" \
  -c:a copy out.mp4
```

`format=rgba` is required before `colorchannelmixer=aa`, because the filter needs an alpha channel to modify and a JPEG does not have one. Skipping it is a common cause of the transparency silently doing nothing.

## Text that stays readable

Plain white text disappears over bright footage. A translucent box behind it fixes that in one option:

```bash
ffmpeg -i base.mp4 \
  -vf "drawtext=text='Rendobar':fontsize=48:fontcolor=white:box=1:boxcolor=black@0.5:boxborderw=12:x=(w-text_w)/2:y=(h-text_h)/2" \
  -c:a copy out.mp4
```

For `drawtext` the variables are lowercase and different from overlay's: `w` and `h` are the **video**, and `text_w` and `text_h` are the rendered text. Mixing up the two sets is the second most common way these commands fail.

Fonts are the other trap. `drawtext` needs a font that exists in the build you are running, and a font present on your laptop is frequently absent from a container image. Passing `fontfile=` with an explicit path removes the ambiguity.

Here is the box option running against the sample, bottom right at 35% opacity:

_A text watermark burned into the picture with drawtext, positioned bottom right with a translucent box so it stays readable over bright footage._

The box is what makes it survive a bright frame. Without `box=1` the same text disappears wherever the footage goes light, which is the failure people notice only after publishing.

## Keeping the audio

Every command here carries `-c:a copy`, which passes the audio through untouched.

Leave it out and FFmpeg re-encodes the audio for no reason, costing time and a generation of quality. Watermarking changes only the video, so the audio should never be touched. This is the cheapest correctness win in the whole operation and it is omitted from most examples online.

## What it costs

Median cost of an overlay job in the archive was **$0.0027**, effectively identical to the **$0.0025** median of all recent FFmpeg jobs. Compositing a logo is close to free relative to the encode it rides along with.

Running it over HTTP is the same command as a string:

const rb = createClient({ apiKey: process.env.RENDOBAR_API_KEY });

const base = "https://cdn.rendobar.com/assets/examples/sample.mp4";
const logo = "https://cdn.rendobar.com/assets/examples/photo.jpg";

const job = await rb.jobs.run({
  type: "ffmpeg",
  params: {
    // filter_complex, not -vf. Two inputs need explicit stream labels.
    command:
      "ffmpeg -i " + base + " -i " + logo +
      ' -filter_complex "[0:v][1:v]overlay=W-w-20:H-h-20" -c:a copy -t 5 out.mp4',
  },
});

console.log(job.output.file.url);`}
  curl={`curl -X POST https://api.rendobar.com/jobs \
  -H "Authorization: Bearer $RENDOBAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "ffmpeg",
    "params": { "command": "ffmpeg -i https://cdn.rendobar.com/assets/examples/sample.mp4 -i https://cdn.rendobar.com/assets/examples/photo.jpg -filter_complex \"[0:v][1:v]overlay=W-w-20:H-h-20\" -c:a copy -t 5 out.mp4" }
  }'`}
/>

## Where this stops

The 40 overlay jobs are our own traffic and include deliberate failures from testing, so the 21-of-40 failure rate describes an experiment log rather than what a working pipeline experiences. What transfers is the **cause** distribution, not the rate: the failures cluster on one mistake, and it is the `-vf` versus `-filter_complex` one.

Every command here was run against short clips at 1280x720. Nothing here measures the cost of watermarking long-form video, where the encode dominates and the overlay itself remains negligible.

This is a visible watermark, which is a branding tool and not a security one. Anyone can crop it out. Invisible watermarking that survives re-encoding is a different technique with different trade-offs and is not what these commands do.

For the errors these commands produce when they go wrong, see [common FFmpeg errors and what they mean](/blog/ffmpeg-errors-explained/). For encoder settings on the output, see [FFmpeg encoding settings measured](/blog/ffmpeg-encoding-settings/).
