Ahosting Logo
Knowledge Base

How to Add Text and Timecode Overlays with drawtext

Two failures account for almost every drawtext command that does not workA missing font· the filter needs a font file path· and a system without one simply errors· the message names the font, not the problemUnescaped punctuation· colons and commas are filter syntax· so a colon inside your text ends the option· and the error appears somewhere unrelatedFor timecodeThe text comes from a timestamp expression rather than a fixed string, which is also where mostof the punctuation lives.

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.