Add a watermark with FFmpeg, and why -vf fails
Watermark a video with FFmpeg using an image or text, with corner positions, logo sizing relative to the frame, and why overlay under -vf always fails.
An image watermark in the bottom right, 20 pixels from each edge, is one overlay in a -filter_complex graph:
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 near the bottom, is one drawtext under -vf:
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.mp4The 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:
# 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.mp4Those 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:
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:
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:
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.mp4In 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:
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 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:
ffmpeg -i base.mp4 \ -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: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.mp4The 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:
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:
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/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);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/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" }}'Returns immediately with a job id. Poll GET /jobs/{id} or register a webhook rather than blocking on the request.
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.
Frequently asked questions
How do I show a watermark only for part of a video in FFmpeg?
Add enable to the overlay with a time expression. overlay=W-w-20:H-h-20:enable='between(t,1,3)' shows the logo from second 1 to second 3 and passes the video through untouched outside that window.
