ffmpegsubtitlesvideo-automation

Extract Subtitles from MKV and MP4 with FFmpeg, No Re-encoding

·Javid Jamae·10 min read
Extract Subtitles from MKV and MP4 with FFmpeg, No Re-encoding

You ran ffmpeg -i movie.mkv -map 0:s:0 subs.srt, got a file, and opened it to find the Hungarian forced track with eleven lines in it. Or FFmpeg exited with a complaint about subtitle encoding and wrote nothing. The one-liner isn't wrong, it's guessing, and a guess that works on your test file breaks on file 40 of a batch.

Quick answer: The extract subtitles FFmpeg command is ffmpeg -i input.mkv -map 0:s:0 -c:s srt subs.srt, which writes the first subtitle track to SRT and never decodes a video frame, so nothing is re-encoded. Run ffprobe -v error -select_streams s -show_entries stream=index,codec_name:stream_tags=language -of csv=p=0 input.mkv first so you map a track you actually want, and know that image-based tracks (hdmv_pgs_subtitle, dvd_subtitle) can't become SRT at all without OCR. If you'd rather not run ffprobe, parse its output, and re-encode a burn-in on a box you maintain, FFmpeg Micro runs the extract-and-burn chain as one API call with no servers to run.

ffprobe tells you what's in the file, so stop hardcoding 0:s:0

Before mapping anything, ask the container what it contains. A retail MKV routinely carries six to ten subtitle tracks: full English, English SDH, forced-narrative English, and then four languages you don't need. 0:s:0 is whichever one the muxer wrote first, which is a coin flip.

ffprobe -v error -select_streams s \
  -show_entries stream=index,codec_name:stream_tags=language,title \
  -of csv=p=0 input.mkv

A typical answer looks like this:

2,subrip,eng,English SDH
3,subrip,eng,English Forced
4,ass,jpn,Signs & Songs
5,hdmv_pgs_subtitle,eng,Full

Two numbers live in that output and they are not the same number. The first column is the absolute stream index, used as -map 0:2. The 0:s:N form counts only subtitle streams, so stream index 2 is 0:s:0, index 3 is 0:s:1, and so on. Mixing them up is how people end up extracting an audio track into a file named english.srt.

Language metadata picks the track for you across a whole library

Mapping by language is what makes this work unattended. FFmpeg supports metadata-based stream selection, so you can ask for the English subtitle track by name instead of by position:

ffmpeg -i input.mkv -map 0:s:m:language:eng -c:s srt english.srt

That selects every subtitle stream tagged eng. If the file has both SDH and forced English tracks, it matches both, and FFmpeg will refuse to write two streams into one .srt file. Add -map 0:s:m:language:eng:0? That syntax doesn't exist. Narrow it instead by combining the language probe above with a specific index, or filter on the title tag in your own code and map the absolute index you found.

The other thing to expect: plenty of files have no language tags. MP4s remuxed by consumer tools often ship und. When the tag is missing, the map matches nothing and FFmpeg exits 1 with Stream map '0:s:m:language:eng' matches no streams, which is a selection problem and not a corrupt file. The same diagnosis logic applies to audio maps that match no streams.

Extracting every track at once beats running the command ten times

One FFmpeg invocation can write many outputs, and since subtitle extraction reads the container rather than decoding video, pulling eight tracks costs about what pulling one costs. A two-hour MKV usually finishes in under two seconds on a laptop, and each English SRT lands somewhere between 60 KB and 120 KB.

ffmpeg -nostdin -v error -i input.mkv \
  -map 0:s:0 -c:s srt sub_0.srt \
  -map 0:s:1 -c:s srt sub_1.srt \
  -map 0:s:2 -c:s srt sub_2.srt

For an unknown number of tracks, drive it from the probe so the filenames carry the language:

ffprobe -v error -select_streams s \
  -show_entries stream=index:stream_tags=language \
  -of csv=p=0 input.mkv |
while IFS=, read -r index lang; do
  ffmpeg -nostdin -v error -i input.mkv \
    -map "0:$index" -c:s srt "sub_${index}_${lang:-und}.srt" \
    || echo "track $index is not text, skipping"
done

That || echo matters more than it looks. One bitmap track in the middle of the list will otherwise kill the loop on a non-zero exit and you'll ship a job that silently produced half its outputs.

Two codecs end the job, and both look like FFmpeg being broken

Subtitle codecs split into text and bitmap, and FFmpeg can convert within a group but never across it. The exact error, which is worth grepping your logs for, is Subtitle encoding currently only possible from text to text or bitmap to bitmap.

codec_nameWhat it isCan it become SRT?
`subrip`Plain text, MKV's defaultYes, `-c:s copy` or `-c:s srt`
`ass` / `ssa`Styled text, Advanced SubStation AlphaYes, styling is dropped
`webvtt`Text, WebM and HLSYes
`mov_text`Text, the MP4 subtitle formatYes, but `-c:s copy` fails
`hdmv_pgs_subtitle`Blu-ray bitmapsNo, needs OCR
`dvd_subtitle`VobSub bitmapsNo, needs OCR

The PGS and VobSub case is the one people fight longest. Those tracks are pictures of words, roughly 5 MB to 20 MB per feature-length track because every line is an image. FFmpeg can copy them out to a .sup or a .sub/.idx pair, and that's the limit of what it does:

ffmpeg -i input.mkv -map 0:s:4 -c:s copy subs.sup

Turning that into SRT is an OCR job, not an FFmpeg job. Subtitle Edit (Windows, with a CLI mode) and the Python tool pgsrip both wrap Tesseract for it, and both need a human to fix the l/I and rn/m confusions afterward. If your pipeline needs text and the source is a disc rip, budget for a review step or skip the embedded track and transcribe the audio instead. FFmpeg 8.0's native Whisper filter is often faster than cleaning up OCR output.

The MP4 case is smaller and more annoying. MP4 stores text subtitles as mov_text, and the SRT muxer won't accept a copied mov_text stream, so the header write fails and you get an empty or missing file. Name the encoder explicitly:

ffmpeg -i input.mp4 -map 0:s:0 -c:s srt subs.srt

Text to text conversion costs nothing measurable. It's a format rewrite, not an encode.

The pipeline case: pull the track, change it, put it back

Extraction on its own is rarely the deliverable. The actual job is usually a chain: pull the existing English SRT out of a source video, run it through translation or a style pass, then burn the result back onto the picture for a platform that ignores sidecar files. Instagram, TikTok, and LinkedIn all fall in that bucket.

Locally that's three commands and one hard cost. Steps one and two are free. Step three re-encodes the video:

ffmpeg -i input.mkv -map 0:s:0 -c:s srt en.srt
# translate or restyle en.srt into styled.ass here
ffmpeg -i input.mkv -vf "subtitles=styled.ass" \
  -c:v libx264 -crf 20 -preset medium -c:a copy output.mp4

Every frame gets decoded, drawn on, and re-encoded, which on a ten-minute 1080p clip is minutes of CPU, not seconds. That's the step that turns a tidy script into a worker queue, a temp-directory cleanup policy, and a timeout you keep raising. It's also the step where the font you assumed exists doesn't, and where the ASS styling quietly reverts. Burn-in is what breaks in translated-subtitle workflows, not the translation.

This is the point where sending the job out makes sense. FFmpeg Micro runs the whole chain as one job against a source URL, extract then restyle then burn, and hands back a finished MP4 with no encoder to host and no long-running process to babysit. The job shapes and the chaining syntax are in the docs, and the free tier covers enough to test the chain against your own file before you wire it into anything.

Pitfalls that cost the most time

Four of these account for most of the failed runs, and none of them announce themselves clearly in the logs.

  1. Writing .srt when the track is ass throws away every style tag. If you need positioning and color preserved, extract to .ass with -c:s copy instead.
  2. Forgetting -vn -an on a loop that maps by absolute index. If your index arithmetic is off by one you'll extract audio into a text container and get a confusing muxer error rather than a clear one.
  3. Assuming timestamps survive a trim. Extracting subtitles from a clip you already cut gives you timings relative to the original, because FFmpeg cuts on keyframes and subtitle packets carry their own PTS.
  4. Running the extraction on a remote file you downloaded first. FFmpeg reads HTTP sources directly, so a presigned URL saves the whole download step.

FAQ

How do I extract SRT from MKV without re-encoding?

Extracting SRT from an MKV never re-encodes video in the first place. ffmpeg -i input.mkv -map 0:s:0 -c:s srt subs.srt selects only the subtitle stream, so no video or audio frames are decoded and the source file is untouched. The -c:s srt part converts subtitle text between formats, which is a text rewrite and not an encode.

Why does my extracted subtitle file come out empty?

An empty subtitle file almost always means FFmpeg mapped a track that isn't the one you wanted, most often a forced-narrative track that legitimately contains only a dozen lines. Run ffprobe with -select_streams s and check the title tag before assuming the extraction failed. The other common cause is a bitmap track, where FFmpeg writes a zero-byte SRT and logs a text-to-bitmap conversion error.

Can FFmpeg convert PGS subtitles to SRT?

FFmpeg cannot convert PGS subtitles to SRT. PGS (hdmv_pgs_subtitle) and VobSub (dvd_subtitle) store each line as an image, and FFmpeg only converts text to text or bitmap to bitmap. Getting SRT out of them requires an OCR tool such as Subtitle Edit or pgsrip, plus a proofreading pass.

How do I extract subtitles from an MP4 instead of an MKV?

Extracting subtitles from an MP4 uses the same -map syntax, with one change: MP4 stores text subtitles as mov_text, and the SRT muxer rejects a straight copy of that codec. Use ffmpeg -i input.mp4 -map 0:s:0 -c:s srt subs.srt rather than -c:s copy. Many MP4s carry no subtitle stream at all, since captions were burned in or delivered as a separate file.

Which subtitle track does 0:s:0 actually select?

0:s:0 selects the first subtitle stream in input file 0, in container order, with no regard for language or type. It's not the English track, the default track, or the largest track. Use -map 0:s:m:language:eng to select by language tag, and fall back to an absolute stream index from ffprobe when the file's tags are missing or wrong.

If the chain past extraction is what's eating your afternoons, wire the extract, restyle, and burn-in steps into a single job and let it run somewhere other than your machine. Sign up free and run it against one of your own files.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Skip the command line

The Auto Captions blueprint transcribes your video and burns the captions in. You just review the transcript.

Run it (free)