Drawing text into video is one filter, drawtext, and it has a reputation for being fiddly. The reputation is deserved, and it comes down to two things.
You need a font file
ffmpeg -i in.mp4 -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf:text='Hello':x=20:y=20:fontsize=32:fontcolor=white" out.mp4
The filter needs a real path to a real font. Servers frequently have none installed, so a command copied from a tutorial fails on a machine that has no fonts at all.
fc-list | head ls /usr/share/fonts/truetype/
If nothing is listed, install a font package before anything else. This is the most common reason a working command from elsewhere does nothing here.
The escaping
The filter separates its own options with colons. A colon inside your text therefore ends the text early and produces a syntax error instead of a timestamp.
Commas, single quotes and percent signs cause the same trouble. Escape them:
text='Recorded at 14\:30'
For anything longer than a few words, avoid the problem entirely by putting the text in a file:
drawtext=fontfile=/path/font.ttf:textfile=caption.txt:x=20:y=20
That removes shell quoting and filter escaping from the equation at once, and it is the right approach whenever the text comes from somewhere else: certainly whenever it comes from a user. Processing untrusted video uploads safely deals with why user-supplied text should never be pasted into a command.
Position
w and h are the video's dimensions; text_w and text_h are the text's. So:
x=(w-text_w)/2:y=h-text_h-20 # centred, near the bottom x=w-text_w-20:y=20 # top right
Add a background box so the text stays readable over any footage:
:box=1:[email protected]:boxborderw=10
Without it, white text over a bright scene disappears, which is only discovered after the encode.
A running timecode
drawtext=fontfile=/path/font.ttf:timecode='00\:00\:00\:00':rate=25:x=20:y=20:fontsize=28:fontcolor=white:box=1:[email protected]
The rate must match the video's frame rate or the counter drifts against the actual footage. Note the escaped colons in the starting value.
For elapsed seconds instead of a broadcast timecode, use the timestamp expression:
drawtext=fontfile=/path/font.ttf:text='%{pts\:hms}':x=20:y=20:fontsize=28:fontcolor=white
Text that appears and disappears
drawtext=...:enable='between(t,5,15)'
Shown between five and fifteen seconds. Several drawtext filters chained with commas, each with its own window, produce simple captions without any editing software.
Understanding FFmpeg filters explains chaining them.
Burned or separate?
This is the decision to make before writing any of it.
Burned text cannot be removed, translated, or turned off. It is part of the picture, and changing it means re-encoding from the source, which you may no longer have.
So burn only what must survive being downloaded and re-uploaded: a watermark, a source attribution, a timecode for review copies.
Anything a viewer might want to switch off or read in another language belongs in a subtitle track, which is metadata and can be changed without touching the video. For that, see working with subtitles in FFmpeg.
Draw at the final size
Scale first, then draw. Text burned at 1920 wide and then scaled to 640 becomes soft and sometimes illegible, because it was resampled along with the picture.
-vf "scale=1280:-2,drawtext=..."
If you are producing several sizes, each needs its own drawing pass at its own font size, which is one of the few cases where processing each rendition separately is genuinely necessary rather than wasteful.
Make the text readable over any picture
Text drawn directly onto video disappears whenever the frame behind it is the same brightness, which on real footage is most of the time somewhere.
ffmpeg -i in.mp4 -vf "drawtext=fontfile=/usr/share/fonts/dejavu/DejaVuSans.ttf:\ text='Ornek':x=40:y=h-80:fontsize=36:fontcolor=white:\ box=1:[email protected]:boxborderw=12" -c:a copy out.mp4
A semi transparent box behind the text solves it in every case and costs nothing. A border or a shadow works too and fails on busy detail where a solid backing does not.
Choose the size relative to the frame rather than in fixed points. Text sized for a large frame is unreadable when the same command runs on a smaller one, which is the usual complaint after a batch.
Check the font actually loaded
A missing font produces an error that names the filter rather than the file, and the command simply fails.
fc-list 2>/dev/null | head -5 ls /usr/share/fonts/ 2>/dev/null ffmpeg -hide_banner -filters | grep drawtext
Give the full path to a file you have confirmed exists rather than a font name, since name resolution depends on a configuration that may not be present on a server.
If the filter itself is missing from the list, the build does not include the text library and no path will help. That is a build matter rather than a command matter, and it is worth checking before writing a pipeline around it. Getting started with FFmpeg covers reading what a build supports.
Draw once, not per output
Text drawn onto video forces a re-encode, so applying it separately to each delivery format encodes the same content several times.
ffmpeg -i master.mp4 -vf "drawtext=..." -c:a copy stamped.mp4 ffmpeg -i stamped.mp4 -vf "scale=1280:-2" -c:v libx264 -crf 23 -c:a copy web720.mp4 ffprobe -v error -show_entries stream=width,height -of csv=p=0 web720.mp4
Apply the overlay once to a master and derive every size from that. The text scales with the picture and the machine time is spent once rather than per format.
The exception is text that has to differ per destination, such as a per customer identifier. That is a genuine reason to draw separately, and it is worth being certain the difference is real before paying for it on every file.
Check what the text looks like on the smallest screen
Overlay text is judged on a large display and read on a phone, where thin strokes disappear entirely.
ffmpeg -ss 10 -i out.mp4 -frames:v 1 -vf "scale=360:-2" small.png ffmpeg -ss 10 -i out.mp4 -frames:v 1 full.png
Extract the same frame at both sizes and compare. A caption that is comfortable at full size and illegible at a third of it needs a larger relative size rather than a different font.
Check three frames from different parts of the video rather than one. The frame you happened to test is rarely the one where the text lands on a bright, busy background.
Keep the text consistent across a library
Overlays applied one file at a time drift in size, position and wording. Put the values in variables and reuse them, so every file carries the same mark in the same place. That consistency is what makes a watermark read as deliberate rather than as something added by whoever processed that particular file. It also means a change to the design is one edit rather than a review of everything already produced.