---
title: "Convert MKV to MP4 in C# (remux, no re-encoding)"
url: https://recursiveai.org/onemedia/cookbook/convert-mkv-to-mp4/
description: "Convert an MKV file to MP4 in C# without re-encoding: copy the H.264 and AAC tracks into MP4 (or MOV, WebM, Matroska) with OneMedia’s remux, and print the report of every track and tag that didn’t carry over. No FFmpeg."
version: "0.3.0-preview.2"
---

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

`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.

```shell
dotnet new console -o MkvToMp4 && cd MkvToMp4
dotnet add package RecursiveAI.OneMedia --version 0.3.0-preview.2
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.Remuxing;

// The output format comes from the extension: .mp4, .m4a, .mov, .3gp, .mkv, .webm.
string input = args.Length > 0 ? args[0] : "test5.mkv";
string output = args.Length > 1 ? args[1] : "test5.mp4";
RemuxReport report = Media.Remux(input, output);

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");

// The video, or all of the audio, didn't make it: the output is not a copy of the input.
if (report.LostMediaTracks.Count > 0)
    Console.WriteLine($"LOST: input track(s) {string.Join(", ", report.LostMediaTracks)}; {output} lacks media the input had");
```

```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: Input 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.LostMediaTracks` is the test that matters: it lists the input’s video tracks, and its audio tracks when none of them was written, that didn’t reach the output for any reason but your own choice. Empty, as here, means the picture and sound came across; the subtitles are listed in `Losses` but not there. `report.HasLosses` is the broader test: anything at all left out. 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

```shell
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.

```shell
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 every frame of the MKV: the edit list runs to the end of the last frame shown.

```shell
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
1104
```

## When the video can’t be carried

MP4 has no place for MPEG-2 video in OneMedia, so a remux of an MPEG-2 MKV writes the audio alone and says the video was lost. Make one with FFmpeg and run the same program on it:

```shell
ffmpeg -v error -f lavfi -i testsrc=d=2:s=320x240 -f lavfi -i sine=d=2 -c:v mpeg2video -c:a aac -shortest clip-mpeg2.mkv
dotnet run -- clip-mpeg2.mkv clip-mpeg2.mp4
```

```text
track 1 -> 0: 88 packets copied
not carried: track 0, Video mpeg2video [V_MPEG2]: UnsupportedCodec
not carried: Tag mkv-info: NotRepresentable
not carried: Tag mkv: NotRepresentable
changed: Input track 1: packet times were moved forward by the priming (23219955/1000000000 s) so the first packet starts at zero; the priming is kept in the edit information.
changed: tags carried through the common view (1 edits, 0 pictures)
18,898 bytes written
LOST: input track(s) 0; clip-mpeg2.mp4 lacks media the input had
```

Check `LostMediaTracks` before you use or publish the output. The `onemedia remux` command does the same check: it prints a warning and exits with 3 when the output lacks media the input had.

## 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](https://recursiveai.org/onemedia/cookbook/make-video-playable-in-browser/) re-encodes it to Opus.
- **Subtitles.** SubRip and other Matroska text tracks don’t go into MP4. Remux to `.mkv` to keep them.
- **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, remux with their video: the Sintel trailer copied to AVI that way comes back as an MP4 with all 1,253 of its frames.
- **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](https://recursiveai.org/onemedia/docs/remux/).

To only move an MP4’s index to the front, see [MP4 faststart](https://recursiveai.org/onemedia/cookbook/mp4-faststart/).
