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.
Short version
An image watermark, bottom right, 20 pixels from each edge:
ffmpeg -i base.mp4 -i logo.png \ -filter_complex "[0:v][1:v]overlay=W-w-20:H-h-20" \ -c:a copy out.mp4A text watermark, centred at the bottom:
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.mp4The 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:
# 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:
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:
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.mp4That 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:
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.mp4format=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:
ffmpeg -i base.mp4 \ -vf "drawtext=text='Rendobar':fontsize=48:fontcolor=white:box=1:[email protected]:boxborderw=12:x=(w-text_w)/2:y=(h-text_h)/2" \ -c:a copy out.mp4For 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:
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:
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.
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:[email protected] keeps it readable over bright footage.
