There are two ways to put subtitles on a video with FFmpeg. Soft subtitles are a separate text track inside the file: players can turn them on and off, and nothing is re-encoded. Burned-in subtitles are drawn into the picture: they show in every player, including social feeds, but the video has to be re-encoded and nobody can turn them off. This page measures both and points out a trap that leaves you with no subtitles and no error.
Measured on 2026-11-07 with FFmpeg 9.0.2 (linuxserver/ffmpeg image, built with libass) on a 10-second 1280×720, 30 fps H.264 video with AAC audio, and a two-line SRT file with Polish accents.
The SRT file
1
00:00:00,500 --> 00:00:02,500
Hello and welcome.
2
00:00:03,000 --> 00:00:05,500
Zażółć gęślą jaźń — subtitles with accents.
SRT is a numbered list of cues: a time range (with a comma before the milliseconds) and one or two lines of text, separated by blank lines. Save it as UTF-8, or accented letters break. Most transcription tools export SRT; WebVTT (.vtt) works the same way in FFmpeg.
Soft subtitles
# MP4: text subtitles must be mov_text
ffmpeg -i in.mp4 -i subs.srt -c copy -c:s mov_text -metadata:s:s:0 language=eng out.mp4
# MKV: keeps SRT as it is
ffmpeg -i in.mp4 -i subs.srt -c copy out.mkv
-c copy copies the video and audio untouched; -c:s mov_text converts the subtitle stream to the text format MP4 supports; the language tag lets players show "English" in their subtitle menu. In our test this took 0.03 seconds and the output had three streams: H.264, AAC and mov_text. The MKV command kept the subtitles as SubRip (subrip).
The trap
Leave out -c:s mov_text:
ffmpeg -i in.mp4 -i subs.srt -c copy out.mp4 # no subtitles in the result
FFmpeg 9.0.2 finished without an error, and the MP4 had only video and audio — the subtitle stream was simply not included. Always check the result with ffprobe -v error -show_entries stream=codec_name -of csv=p=0 out.mp4; a file with subtitles lists mov_text.
Soft subtitles are the right format for YouTube and Vimeo (both also accept the SRT file uploaded on its own), for video on your website (a <track> element with a .vtt file, which also lets you offer several languages), and for archives, where the text stays searchable and editable.
Converting and extracting
FFmpeg also converts between subtitle formats: ffmpeg -i subs.srt subs.vtt turned our SRT into WebVTT for a web <track> in one step, and ffmpeg -i soft.mp4 -map 0:s:0 back.srt pulled the mov_text track out of the MP4 again as an SRT file with the original timings — handy when you need to correct a typo and re-mux.
Burned-in subtitles
ffmpeg -i in.mp4 -vf "subtitles=subs.srt:force_style='FontSize=28,Outline=2'" -c:a copy out.mp4
The subtitles filter uses libass to draw each cue onto the frames at its time; -c:a copy keeps the audio as it is. This re-encodes the video: 1.38 seconds for our 10-second clip, against 0.03 for the soft track — about 45 times longer, and it scales with the length of your video.
Styling
force_style takes ASS style fields, separated by commas:
| Field | Example | Effect |
|---|---|---|
FontName |
FontName=DejaVu Sans |
A font installed on the machine running FFmpeg |
FontSize |
FontSize=28 |
Size, relative to the script resolution |
PrimaryColour |
PrimaryColour=&H00FFFFFF |
Text colour as &HAABBGGRR (alpha, blue, green, red) |
Outline / Shadow |
Outline=2,Shadow=0 |
Outline and shadow width |
BorderStyle |
BorderStyle=3 |
An opaque box behind the text instead of an outline |
MarginV |
MarginV=40 |
Distance from the bottom edge |
Two surprises. First, sizes are not in video pixels: SRT has no resolution, so libass uses a small default script resolution and scales it to the frame — FontSize=28 drew letters about 60 pixels tall on our 720p video. Start around 20–24 and look at a frame. Second, colours are written blue-green-red: yellow is &H0000FFFF, not &H00FFFF00.
To check a burned frame without watching the whole video: ffmpeg -ss 1 -i out.mp4 -frames:v 1 check.png.
Fonts
The filter can only use fonts installed where FFmpeg runs. In a slim Docker image, text may fall back to another font or show boxes for missing characters — in our image the Polish accents rendered correctly with its installed fonts. If you need a specific font, install it, or point libass at a folder with subtitles=subs.srt:fontsdir=./fonts.
Which to use
| Soft subtitles | Burned in | |
|---|---|---|
| Time (10 s clip) | 0.03 s | 1.38 s |
| Re-encodes video | No | Yes |
| Viewer can turn off | Yes | No |
| Shows in social feeds | Often not | Always |
| Several languages | Yes, one track each | One per video file |
| Searchable text | Yes | No |
For a short clip that will autoplay muted in a feed, burn it in. For long videos, YouTube and your own site, keep subtitles soft or upload the SRT separately.
Quotes from your transcript, as images
A subtitle file is also a ready list of your best lines. We do not caption or edit video — FFmpeg is the tool for that — but the same text makes good images: a quote card for each memorable line of an interview or podcast episode, posted next to the clip. With Shareglint that is one request per line, with the guest's name and the show's cover:
curl -X POST "https://shareglint.com/v1/images?wait=true" \
-H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
-d '{ "template": "podcast-quote", "size": "square",
"modifications": { "quote": "Hello and welcome.", "guest": "Ada Lovelace" } }'
See quote cards for pulling lines from transcripts and the templates that fit.