There are two entirely different ways to put subtitles on a video, and choosing wrong means re-encoding everything later. Soft subtitles are a separate track the viewer can turn off. Burned-in subtitles are painted into the picture permanently.
Soft subtitles: a track alongside the video
The subtitle file is carried in the container. The player renders it, the viewer can turn it off or pick a language, and the video itself is untouched, so this is a stream copy and takes seconds.
ffmpeg -i input.mp4 -i subs.srt \ -c copy -c:s mov_text \ -metadata:s:s:0 language=eng output.mp4
MP4 requires the mov_text subtitle codec. It will not carry SRT directly, and omitting that is the usual reason this command fails. MKV is less fussy:
ffmpeg -i input.mkv -i subs.srt -c copy -c:s srt output.mkv
Several languages, each labelled:
ffmpeg -i input.mp4 -i en.srt -i tr.srt \ -map 0 -map 1 -map 2 -c copy -c:s mov_text \ -metadata:s:s:0 language=eng \ -metadata:s:s:1 language=tur output.mp4
Set the language metadata. Without it players show "Track 1" and "Track 2", which is useless to a viewer choosing between them.
Burned-in subtitles: painted into the picture
Permanent, always visible, and they survive any platform that ignores subtitle tracks, which is most social video.
ffmpeg -i input.mp4 -vf "subtitles=subs.srt" \ -c:v libx264 -crf 20 -c:a copy output.mp4
This re-encodes the video, because you are changing the picture. It takes real time and costs a generation of quality, so set a CRF rather than accepting a default.
Styling, if the source is plain SRT:
ffmpeg -i input.mp4 \ -vf "subtitles=subs.srt:force_style='FontSize=24,PrimaryColour=&HFFFFFF&,BackColour=&H80000000&,BorderStyle=3'" \ -c:v libx264 -crf 20 -c:a copy output.mp4
BorderStyle=3 puts a box behind the text, which is what makes subtitles readable over a bright scene. Without it, white text on a white background disappears exactly when it matters.
Which to choose
Soft for your own site, for a player you control, and whenever the viewer might want them off. It is a stream copy, so it is nearly free and reversible.
Burned-in for social platforms that strip subtitle tracks, for autoplay-muted contexts where subtitles are the only way the content works, and when you need certainty that they appear.
If you need both, produce the soft version first and burn from the same source, not from an already-encoded copy.
Extracting subtitles from a file
ffprobe -hide_banner input.mkv # list the streams ffmpeg -i input.mkv -map 0:s:0 subs.srt # extract the first subtitle track
If the source subtitles are image-based rather than text, common on disc rips. This fails, because there is no text to extract. Those need optical recognition, which FFmpeg does not do.
Fixing timing
Subtitles arriving consistently early or late are shifted with an offset applied to the subtitle input:
ffmpeg -i input.mp4 -itsoffset 2.5 -i subs.srt -c copy -c:s mov_text output.mp4
That delays them by 2.5 seconds. A negative value moves them earlier.
This only fixes a constant offset. Subtitles that drift progressively out of sync are a frame rate mismatch between the subtitle file and the video, and shifting will not help. The file needs regenerating against the correct rate.
What goes wrong
"Subtitle codec not supported." MP4 needs mov_text. Add -c:s mov_text.
Characters render as symbols. The SRT is not UTF-8. Convert the file's encoding before using it. This is common with subtitles produced on Windows in a non-Latin language.
Burned-in subtitles do not appear. Usually a path problem in the filter. Special characters and spaces in the filename need escaping; renaming the file to something plain is faster than working out the escaping.
They appear but are unreadable. No border or box. Add BorderStyle=3.
Soft subtitles do not show on the site. The container carries them and the player is not configured to offer them. That is a player question instead of an FFmpeg one, and it is the reason burned-in exists.
When text must survive being downloaded and re-uploaded it has to be drawn into the picture instead. How to Add Text and Timecode Overlays with drawtext explains that, and when not to.
Read what is already in the file
Before adding anything, find out what tracks exist, because a file frequently already carries several.
ffprobe -v error -select_streams s -show_entries stream=index,codec_name:stream_tags=language,title \ -of csv=p=0 input.mkv ffprobe -v error -show_entries stream=index,codec_type -of csv=p=0 input.mkv
The language tag is what a player uses to label the track and to choose a default. A file with three subtitle tracks and no tags shows the viewer three identical entries, which is a tagging problem rather than a subtitle problem.
Setting the tag costs nothing and is the difference between a usable file and one where the viewer picks by trial and error.
Choose the format the destination supports
Subtitle formats are not interchangeable, and a mismatch produces a track that exists and never appears.
ffmpeg -i input.mkv -map 0:s:0 -c:s srt subs.srt ffmpeg -i input.mp4 -i subs.srt -c copy -c:s mov_text -metadata:s:s:0 language=tur output.mp4
The simpler text format is widely supported and carries no styling. Richer formats carry positioning and styling and are supported in fewer places, particularly in browsers.
For web delivery the format has to be converted again, and served with the correct type. A track that downloads as plain text rather than as subtitles is a server configuration matter rather than a conversion one.
Confirm the track survived the conversion
Subtitle tracks are dropped silently more often than any other stream, because the default mapping does not always include them.
for f in input.mkv output.mp4; do printf '%-14s %s\n' "$f" "$(ffprobe -v error -select_streams s -show_entries stream=index -of csv=p=0 "$f" | wc -l)" done
Compare the counts. A file that went in with two tracks and came out with none had them dropped by the container or by the mapping, and the command reported success either way.
The usual cause is a target container that cannot hold the format being copied. Converting the subtitle codec rather than copying it resolves it, and checking the count afterwards is what tells you it worked. Using ffprobe covers reading the streams.
Match the subtitle timing to the video you actually have
Subtitles obtained separately from the video are frequently timed to a different edit, and the offset is constant rather than random.
ffmpeg -itsoffset 2.5 -i subs.srt -c copy shifted.srt ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4
Compare the last subtitle's timestamp against the video duration. If the subtitles end well before the video does, they belong to a shorter cut and shifting alone will not fix them.
A constant offset is a simple shift. A drift that grows through the file is a frame rate mismatch, and that needs a rate conversion rather than a shift. Telling them apart takes two checks, at the start and near the end.
Serve them correctly on the web
A subtitle file that downloads instead of displaying is a server configuration matter rather than a conversion one.
curl -sI https://example.com/subs.vtt | grep -i content-type ffmpeg -i subs.srt subs.vtt head -3 subs.vtt
The web format must begin with its own header line, and the file has to be served with the correct type or the browser treats it as plain text.
Where the video and the subtitles are on different hosts, the browser also refuses to load the track unless the other host permits it explicitly. That produces a track that exists, resolves, and never appears, with the reason visible only in the browser console.