---
title: "Extract subtitles from MKV in C#"
url: https://recursiveai.org/onemedia/cookbook/extract-subtitles-from-mkv/
description: "Extract subtitles from an MKV file in C#: OneMedia lists every subtitle track (and captions inside the video) and writes each as SRT, WebVTT or ASS, with a report of what the format couldn’t hold. Compared with ffmpeg: byte-identical SRT. A preview."
version: "0.6.0-preview.1"
---

# Extract subtitles from MKV in C#

`media.Subtitles` lists every subtitle source in a video file, in one cue model; `Subtitles.Write(track.Document, "out.srt")` saves one in the format the extension names. Subtitles are a preview in 0.6.0-preview.1. The input is `test5.mkv` from the Matroska project’s test files (cut from Elephants Dream, CC BY: © copyright 2006, Blender Foundation / Netherlands Media Art Institute / www.elephantsdream.org), with eight SubRip subtitle tracks.

```shell
dotnet new console -o MkvSubs && cd MkvSubs
dotnet add package RecursiveAI.OneMedia --version 0.6.0-preview.1
curl -L -o test5.mkv https://raw.githubusercontent.com/ietf-wg-cellar/matroska-test-files/master/test_files/test5.mkv
```

Put this in `Program.cs` and run `dotnet run`:

```csharp
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Captions;

using MediaFile media = Media.Open("test5.mkv");

// Every subtitle source in the file: text and bitmap tracks, then teletext pages and
// CEA-608/708 captions found in the video (finding those reads the video once).
foreach (SubtitleTrack sub in media.Subtitles)
{
    string path = $"test5.track{sub.Track.Index}.{sub.Language ?? "und"}.srt";
    ConversionReport report = Subtitles.Write(sub.Document, path);
    Console.WriteLine($"{sub.Kind} {sub.Codec} track {sub.Track.Index} ({sub.Language}): {sub.Cues.Count} cues -> {path}");
    foreach (ConversionLoss loss in report.Losses)
        Console.WriteLine($"  {loss.Kind} {loss.Feature}: {loss.Message}");
}
```

```text
Text S_TEXT/UTF8 track 2 (eng): 5 cues -> test5.track2.eng.srt
  Dropped Language: The document language is not carried.
Text S_TEXT/UTF8 track 3 (hun): 6 cues -> test5.track3.hun.srt
  Dropped Language: The document language is not carried.
Text S_TEXT/UTF8 track 4 (ger): 6 cues -> test5.track4.ger.srt
  Dropped Language: The document language is not carried.
Text S_TEXT/UTF8 track 5 (fre): 6 cues -> test5.track5.fre.srt
  Dropped Language: The document language is not carried.
Text S_TEXT/UTF8 track 6 (spa): 6 cues -> test5.track6.spa.srt
  Dropped Language: The document language is not carried.
Text S_TEXT/UTF8 track 7 (ita): 6 cues -> test5.track7.ita.srt
  Dropped Language: The document language is not carried.
Text S_TEXT/UTF8 track 9 (jpn): 6 cues -> test5.track9.jpn.srt
  Dropped Language: The document language is not carried.
Text S_TEXT/UTF8 track 10 (und): 6 cues -> test5.track10.und.srt
```

```shell
head -11 test5.track2.eng.srt
```

```text
1
00:00:03,549 --> 00:00:05,290
...the colossus of Rhodes!

2
00:00:05,757 --> 00:00:06,678
No!

3
00:00:06,715 --> 00:00:12,544
The colossus of Rhodes
```

Each track’s cues are read on first use. The one loss per track is the language, which an SRT file has no place for; it is in the file name here. Write `.vtt` or `.ass` instead and the same cues come out in that format, with the report naming what it couldn’t carry ([Convert SRT to WebVTT](https://recursiveai.org/onemedia/cookbook/convert-srt-to-webvtt/)). `sub.Kind` is `Text` for these tracks; it is `Bitmap` for PGS, VobSub and DVB pictures, `Teletext` for DVB teletext pages, and `Cea608`/`Cea708` for closed captions in the video.

To list the tracks without reading any media data, use `media.ContainerSubtitles`: the text and bitmap tracks only. `media.Subtitles` also scans each H.264, HEVC or MPEG-2 video track once for captions, which for a long file is a full read of the video.

## Compared with ffmpeg

ffmpeg extracts the same tracks with `-map 0:s:N`. Each file is compared with ours byte for byte:

```shell
i=0
for t in 2 3 4 5 6 7 9 10; do
  ours=$(ls test5.track$t.*.srt)
  ffmpeg -v error -y -i test5.mkv -map 0:s:$i ffmpeg.s$i.srt
  echo "$ours: ours $(grep -c -- '-->' $ours) cues, ffmpeg $(grep -c -- '-->' ffmpeg.s$i.srt) cues, $(cmp -s $ours ffmpeg.s$i.srt && echo identical || echo different)"
  i=$((i+1))
done
```

```text
test5.track2.eng.srt: ours 5 cues, ffmpeg 5 cues, identical
test5.track3.hun.srt: ours 6 cues, ffmpeg 6 cues, identical
test5.track4.ger.srt: ours 6 cues, ffmpeg 6 cues, identical
test5.track5.fre.srt: ours 6 cues, ffmpeg 6 cues, identical
test5.track6.spa.srt: ours 6 cues, ffmpeg 6 cues, identical
test5.track7.ita.srt: ours 6 cues, ffmpeg 6 cues, identical
test5.track9.jpn.srt: ours 6 cues, ffmpeg 6 cues, identical
test5.track10.und.srt: ours 6 cues, ffmpeg 6 cues, identical
```

All eight are byte-identical to ffmpeg 8.0.1’s, with the same cues. The project compared its readers and writers with ffmpeg 7.0.2 for SRT, WebVTT, ASS and LRC.

## From the command line

`onemedia subs info test5.mkv` lists the entries and `onemedia subs test5.mkv --to eng.srt --track 0` extracts one; `--all --to <dir>` extracts every entry (bitmap entries as PNG files with an `index.tsv`).

## Limits

- **A preview.** The README on speed: “subtitle parse and convert at 20,000 and 100,000 cues, against SubtitlesParser and Subtitle Edit’s library, come out at a geometric mean of 0.47× (x64) and 0.52× (arm64) of the fastest comparator, worst row 1.21×; SRT and large ASS parsing are close to Subtitle Edit’s time, not ahead” ([x64](https://recursiveai.org/onemedia/docs/bench-x64-0-5/), [arm64](https://recursiveai.org/onemedia/docs/bench-arm64-0-5/)). The API may change before 1.0.
- **Captions.** CEA-608/708 captions are read from H.264, HEVC and MPEG-2 video. In the README’s words, “**The CTA-608/708 and SCTE 35 standards were not available** (they need an account): caption parts rest on the ATSC A/53 and A/72 documents, the US regulation 47 CFR 79.101 and black-box ccextractor and ffmpeg output”; “708 window and pen semantics rest on oracle output alone”. They were not run for this page.
- **Bitmap subtitles** (PGS, VobSub, DVB) come out as RGBA images with position and timing, not text: there is no OCR. Writing one to a text format drops the images, and the report says “Image cue dropped”.
- **Into MP4.** A remux to MP4 converts SubRip to MP4 timed text; in 0.6 all eight of `test5.mkv`’s tracks arrive ([Convert MKV to MP4](https://recursiveai.org/onemedia/cookbook/convert-mkv-to-mp4/)). In 0.4 only one did.
