# Add a watermark with FFmpeg, and why -vf fails

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

---

## Key takeaways

- An image watermark needs -filter_complex. overlay takes two pictures, and a -vf graph only ever has one.
- Failed overlay jobs mostly came back with FFmpeg's generic Invalid argument, which names neither the filter nor the missing input.
- Text is the opposite case. drawtext draws its own pixels from one input, so -vf is right, and about 5 in 6 drawtext jobs completed.
- To size a logo against the video, give the scale filter the video as a reference (rw on FFmpeg 7.1 and later, scale2ref before that). iw scales it against itself.

An image watermark in the bottom right, 20 pixels from each edge, is one `overlay` in a `-filter_complex` graph:

```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 near the bottom, is one `drawtext` under `-vf`:

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

**The image version needs `-filter_complex` and the text version takes `-vf`.** Swap them and the image command cannot work. `-c:a copy` in both passes the audio through untouched, since a watermark changes only the picture.

## Why overlay jobs fail

About two in three failed overlay jobs in Rendobar's job history returned this and nothing more:

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

Nothing in that message points at the cause. The most common failed command looked like this, submitted with three different positions in turn:

```bash
# Cannot work. The graph has one input and overlay needs two.
ffmpeg -i base.mp4 -vf "overlay=W-w-10:H-h-10" -t 5 out.mp4
```

Those jobs stopped within about 30 ms of FFmpeg starting, before a frame was read. The cause is structural. `overlay` composites two video streams. `-vf` builds a simple filter graph with exactly one input, the stream it is attached to. Adding `-i logo.png` does not help, because `-vf` still cannot see it, and no coordinate after the equals sign changes that.

`-filter_complex` can reference several inputs, and labels say which is 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 and `[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.

## The four corners

Placement uses variables, which keeps one command working across resolutions. For `overlay`, `W` and `H` are the main video's dimensions and `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` |

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

## Sizing the logo to the video

A logo at its native size is tiny on 4K footage and huge on 480p. The fix is to scale it against the video's width, which means the scale filter has to see the video.

On FFmpeg 7.1 and later, `scale` accepts the video as a second reference input and exposes its size as `rw` and `rh`:

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

`[1:v][0:v]scale=...` scales the logo (the first input) with the video (the second) as its reference. `w=rw*0.15` sets the logo to 15% of the video's width, and `h=-1` keeps the logo's aspect ratio. I ran the scale step with ffmpeg 8.0 on a 400 by 200 logo: it came out 192 by 96 against the 1280 by 720 sample and 576 by 288 against a 3840 by 2160 frame, the same share of the width on both.

Older builds do the same with `scale2ref`, which outputs both the scaled logo and the untouched reference:

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

In `scale2ref`, `iw` is the reference video's width and `mdar` the logo's display aspect ratio, so `h=ow/mdar` keeps the logo's shape. It still works on ffmpeg 8.0 but logs `scale2ref is deprecated, use scale=rw:rh instead`. Every label you define must be consumed, which is why `[base]` feeds the overlay here.

An earlier version of this section used `scale=iw*0.15:-1`. In that position `iw` is the logo's own width, so the logo came out the same pixel size on every video, the problem the section set out to fix.

## 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` comes before `colorchannelmixer=aa`, because the filter needs an alpha channel to modify and a JPEG has none. Skip it and the transparency does nothing.

## Text that stays readable

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

```bash
ffmpeg -i base.mp4 \
  -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: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
```

The variables differ from overlay's. In `drawtext`, lowercase `w` and `h` are the video, and `text_w` and `text_h` are the rendered text. Carrying `W-w` over from an overlay command puts the text in the wrong place.

`fontfile=` names a font by path, so the command draws the same text on any machine that has that file. The path above is DejaVu Sans on Debian and Ubuntu. Without `fontfile`, drawtext asks the system font configuration for a default, and a minimal container image may not have one.

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 survives a bright frame. Without `box=1` the same text vanishes wherever the footage goes light.

## Running it over HTTP

An overlay job's median cost in the job history was $0.0027, close to the $0.0025 median FFmpeg-command job since August 2026. The logo rides along with an encode you were paying for anyway. The first command on this page runs as written, here with a 192 px logo on the sample clip:

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/brand/web-app-manifest-192x192.png";

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/brand/web-app-manifest-192x192.png -filter_complex \\"[0:v][1:v]overlay=W-w-20:H-h-20\\" -c:a copy -t 5 out.mp4" }
  }'`}
/>

## Limits

The failure shares above describe which mistakes reached the API, not how often overlay fails. The `-vf` failures were a few corner positions submitted over and over, which is what retrying a command that cannot work looks like. The completed jobs ran on short clips, so nothing here measures watermarking long-form video, where the encode dominates cost.

This is a visible watermark, a branding tool. Anyone can crop it out. If you need a mark that survives cropping and re-encoding, that is invisible watermarking, a different technique with its own trade-offs. For a visible logo, I would scale it against the frame with `rw`, keep a 2% inset, and ship the `-filter_complex` command above.
