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.4.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.
dotnet new console -o MkvSubs && cd MkvSubs dotnet add package RecursiveAI.OneMedia --version 0.4.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:
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 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
head -11 test5.track2.eng.srt1 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). 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:
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
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: “Not yet at the speed bar: subtitle parse and convert are not published against SubtitlesParser or Subtitle Edit’s library, and tuning is in progress, so no speed claim is made.” 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, but in 0.4.0-preview.1 only one of
test5.mkv’s eight tracks arrived (Convert MKV to MP4); extracting them as files, as here, gets all eight.