Recursive AI

Convert MKV to MP4 in C# (remux, no re-encoding)

All onemedia recipes

Media.Remux copies the tracks of a video into another container without touching the compressed data, so MKV to MP4 takes seconds and loses no quality. It returns a RemuxReport that names every track, tag or field the MP4 couldn’t hold. 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): H.264 video, two AAC tracks and eight SubRip subtitle tracks.

Terminal
dotnet new console -o MkvToMp4 && cd MkvToMp4
dotnet add package RecursiveAI.OneMedia --version 0.3.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:

C#
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Remuxing;
 
// The output format comes from the extension: .mp4, .m4a, .mov, .3gp, .mkv, .webm.
RemuxReport report = Media.Remux("test5.mkv", "test5.mp4");
 
foreach (RemuxTrackReport track in report.Tracks)
    if (track.OutputTrack >= 0)
        Console.WriteLine($"track {track.InputTrack} -> {track.OutputTrack}: {track.Packets} packets copied");
 
foreach (RemuxLoss loss in report.Losses)
{
    string what = loss.InputTrack >= 0 ? $"track {loss.InputTrack}, {loss.Subject}" : $"{loss.Kind} {loss.Subject}";
    Console.WriteLine($"not carried: {what}: {loss.Reason}");
}
foreach (string change in report.Changes)
    Console.WriteLine($"changed: {change}");
 
Console.WriteLine($"{report.Mux.BytesWritten:N0} bytes written");
Text
track 0 -> 0: 1104 packets copied
track 1 -> 1: 2156 packets copied
track 8 -> 2: 991 packets copied
not carried: track 2, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: track 3, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: track 4, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: track 5, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: track 6, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: track 7, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: track 9, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: track 10, Subtitle subrip [S_TEXT/UTF8]: UnsupportedCodec
not carried: Tag mkv-info: NotRepresentable
changed: Track 0: decode times were derived from the order of the presentation times, because the input stores none; presentation lags decoding by 42 of 1000 s at most.
changed: tags carried through the common view (4 edits, 0 pictures)
31,801,694 bytes written

The video and both audio tracks are copied packet for packet; MP4 has no place for SubRip text from Matroska, so the eight subtitle tracks are left out and listed with the reason UnsupportedCodec. Title, comment, date and encoder are carried over through the common tag view; the two Matroska Segment fields with no common field (MuxingApp, DateUTC) are listed as a Tag loss, and loss.Detail names them. Matroska stores no decode times, so the remux derives them from the presentation order and says so in Changes.

report.HasLosses is the one-line test. To fail instead of dropping a track, pass RemuxOptions with AllowDrop = false to Remuxer.Remux; Select and Edits on the same options pick tracks and change their language, name, default flag or display rotation. The other direction is the same call: Media.Remux("in.mp4", "out.mkv").

Check it with ffprobe

Terminal
ffprobe -v error -show_entries stream=index,codec_name,codec_type:format=format_name,duration -of compact test5.mp4
Text
stream|index=0|codec_name=h264|codec_type=video
stream|index=1|codec_name=aac|codec_type=audio
stream|index=2|codec_name=aac|codec_type=audio
format|format_name=mov,mp4,m4a,3gp,3g2,mj2|duration=46.030000

The copied video packets are byte-identical to the MKV’s: ffprobe prints the same size and payload hash for each of the 1,104 packets in both files.

Terminal
for f in test5.mkv test5.mp4; do ffprobe -v error -select_streams v:0 -show_entries packet=size -show_data_hash MD5 -of csv $f | md5sum; done
Text
235955a543fd96fab158e65ddaac86f1  -
235955a543fd96fab158e65ddaac86f1  -

Decoded, the MP4 shows one frame fewer than the MKV. The MP4’s edit list ends 42 ms early, which is the decode delay the remux derived (the changed: line above), so a player that follows edit lists stops before the last frame:

Terminal
for f in test5.mkv test5.mp4; do ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of csv=p=0 $f; done
Text
1104
1103

Limits

  • No re-encoding. A codec the target can’t hold is left out, never converted: MPEG-2 or MPEG-4 Part 2 video, VC-1, and Vorbis or WMA into MP4; anything but VP8, VP9, AV1, Vorbis and Opus into WebM. To fix audio for a browser, Make a video play in browsers re-encodes it to Opus.
  • Subtitles. SubRip and other Matroska text tracks don’t go into MP4. Remux to .mkv to keep them.
  • The last frames. In 0.3.0-preview.1 a Matroska video with B-frames comes out of the remux into MP4 one or two frames short at the end, as above: the edit list is shortened by the derived decode delay. All packets are copied; only the shown range is short.
  • Matroska output doesn’t carry AAC priming, rotation, interlacing or stereo mode, and chapters are not written by a remux in either direction.
  • Input formats. Anything OneMedia reads can be the input, MPEG-TS included (its Annex B H.264 and ADTS AAC are re-framed for MP4, not re-encoded). AVI files that store H.264 as avc1, as ffmpeg -c copy from an MP4 writes them, lose the video track in 0.3.0-preview.1 (MissingConfiguration): check report.Losses.
  • Writing. The output is created new and deleted if the remux fails; set Overwrite in RemuxOptions to replace a file. Matroska and WebM writing is a preview in 0.3. Rules and test results: Remux and faststart.

To only move an MP4’s index to the front, see MP4 faststart.