How to add a watermark with FFmpeg

Add an image or text watermark to a video, with copyable corner positions and why 21 of 40 overlay jobs in our archive failed outright.

Share

Short version

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

Terminal window
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:

Terminal window
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:

Terminal window
# 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:

Terminal window
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.

PositionExpression
Top leftoverlay=20:20
Top rightoverlay=W-w-20:20
Bottom leftoverlay=20:H-h-20
Bottom rightoverlay=W-w-20:H-h-20
Centredoverlay=(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:

Terminal window
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:

Terminal window
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:

Terminal window
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:

drawtext, box at black@0.35, bottom right
A text watermark burned into the picture with drawtext, positioned bottom right with a translucent box so it stays readable over bright footage. Encoded on the Rendobar API on 2026-08-21, 5 seconds of the same public sample.

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:

job.ts
import { createClient } from "@rendobar/sdk";
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);

Install with npm i @rendobar/sdk. jobs.run() submits and waits, so it returns the finished job in one call.

terminal
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" }
}'

Returns immediately with a job id. Poll GET /jobs/{id} or register a webhook rather than blocking on the request.

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. For encoder settings on the output, see FFmpeg encoding settings measured.

Frequently asked questions

How do I add a watermark to a video with FFmpeg?

For an image, pass both files with -i and combine them with -filter_complex "[0:v][1:v]overlay=W-w-20:H-h-20". For text, a single -vf drawtext filter is enough because drawtext does not need a second input.

Why does my FFmpeg overlay command fail?

Almost always because it uses -vf instead of -filter_complex. Overlay needs two inputs and -vf only sees the first, so no coordinate value will make it work. In our archive 21 of 40 overlay jobs failed, mostly this way.

How do I position a watermark in the corner of a video?

Use the built-in variables. W and H are the main video's width and height, w and h are the overlay's. Bottom right with a 20px margin is overlay=W-w-20:H-h-20, and centred is overlay=(W-w)/2:(H-h)/2.

How do I make a watermark semi-transparent in FFmpeg?

Lower the overlay image's alpha before compositing with format=rgba then colorchannelmixer=aa=0.5, which multiplies its alpha by 0.5. A PNG with transparency also composites correctly on its own without any extra filter.

Can I add a text watermark without an image file?

Yes, drawtext renders text directly. It needs a font available to the build, and the text, size, colour and position are all filter options. A box behind the text with box=1:boxcolor=black@0.5 keeps it readable over bright footage.

Sources

Tags #ffmpeg#watermark#overlay#drawtext
All posts
Share
  1. Video APIs that deliver output to your own bucket Guides for the video API
  2. Give a service S3 access without an access key Guides for the video API
  3. Add video processing to n8n Cloud Guides for the video API