---
title: "Convert MP4 to MPEG-TS in C# (no re-encoding)"
url: https://recursiveai.org/onemedia/cookbook/convert-mp4-to-ts/
description: "Convert an MP4 to an MPEG transport stream (.ts or .m2ts) in C# without re-encoding: OneMedia’s remux writes H.264 and AAC as Annex B and ADTS in a TS, with a report of what a TS can’t hold. Checked frame by frame with ffmpeg. No FFmpeg needed."
version: "0.7.0-preview.1"
---

# Convert MP4 to MPEG-TS in C# (no re-encoding)

`Media.Remux("in.mp4", "out.ts")` copies the tracks of a video into an MPEG transport stream: one program with PAT and PMT, PES packets with PTS and DTS, and a PCR, written strictly forward. H.264 and HEVC get start codes in place of their length prefixes and the parameter sets in front of each keyframe, and AAC gets ADTS headers; the coded pictures and audio frames themselves are copied, not re-encoded. Use `.m2ts` or `.mts` for 192-byte M2TS packets. TS and M2TS writing are new in 0.6.0-preview.1.

```shell
dotnet new console -o Mp4ToTs && cd Mp4ToTs
dotnet add package RecursiveAI.OneMedia --version 0.7.0-preview.1
curl -L -o sintel.mp4 https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4
```

The input is the Sintel trailer (© copyright Blender Foundation, durian.blender.org, CC BY 3.0): H.264 video and AAC audio. Put this in `Program.cs` and run `dotnet run`:

```csharp
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Remuxing;

// The extension picks the target: .ts (188-byte packets), .m2ts or .mts (192-byte packets).
string input = args.Length > 0 ? args[0] : "sintel.mp4";
string output = args.Length > 1 ? args[1] : "sintel.ts";
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)
    Console.WriteLine($"not carried: {loss.Kind} {loss.Subject}: {loss.Reason}: {loss.Detail}");
foreach (string change in report.Changes)
    Console.WriteLine($"changed: {change}");
Console.WriteLine($"{report.Mux.BytesWritten:N0} bytes written");

if (report.LostMediaTracks.Count > 0)
    Console.WriteLine($"LOST: input track(s) {string.Join(", ", report.LostMediaTracks)}");
```

```text
track 0 -> 0: 1253 packets copied
track 1 -> 1: 2435 packets copied
not carried: Tag ilst: NotWritten: 5 tags: ©nam, ©ART, ©too, ©cpy, desc.
not carried: Disposition Default: NotRepresentable: A transport stream has no field for track dispositions.
not carried: Disposition Default: NotRepresentable: A transport stream has no field for track dispositions.
changed: Input track 0: packet bytes differ from the input by design: length prefixes became Annex B start codes, and the parameter sets from the avcC record are written in-band before keyframes that lack them (plus an access unit delimiter in front of each access unit, unless switched off).
changed: Input track 0: times were moved by -2/24 s, the start of its first edit and its priming, because a transport stream has no edit list.
changed: Input track 1: packet bytes differ from the input by design: an ADTS header was added in front of each AAC access unit.
5,083,332 bytes written
```

Every packet of both tracks is written. A transport stream has no place for the MP4’s iTunes tags or for track dispositions, so the report lists them. `Changes` says what the remux did to the data it carried: the video’s start codes and parameter sets, the audio’s ADTS headers, and, because a transport stream has no edit list, the video’s start offset moved into the packet times.

## Check it with ffmpeg

```shell
ffprobe -v error -count_packets -show_entries stream=codec_name,nb_read_packets,duration:format=format_name,duration -of compact sintel.ts
```

```text
program|stream|codec_name=h264|duration=52.208333|nb_read_packets=1253
stream|codec_name=aac|duration=51.946667|nb_read_packets=2435

stream|codec_name=h264|duration=52.208333|nb_read_packets=1253
stream|codec_name=aac|duration=51.946667|nb_read_packets=2435
format|format_name=mpegts|duration=52.208333
```

The audio packets differ from the MP4’s by their ADTS headers, and the video by its start codes and parameter sets, so compare what they decode to. Every one of the 1,253 pictures and the whole soundtrack decode identically from both files:

```shell
for f in sintel.mp4 sintel.ts; do
  echo "$f video $(ffmpeg -v error -i $f -map 0:v:0 -f framemd5 - | grep -v '^#' | cut -d, -f6 | md5sum)"
  echo "$f audio $(ffmpeg -v error -i $f -map 0:a:0 -f s16le - | md5sum)"
done
```

```text
sintel.mp4 video 8290074c3615185b9bfb28105932785d  -
sintel.mp4 audio d58b078f881cebc4d6b932a6cbf62924  -
sintel.ts video 8290074c3615185b9bfb28105932785d  -
sintel.ts audio d58b078f881cebc4d6b932a6cbf62924  -
```

Browsers don’t play a `.ts` file in a `<video>` element (Chrome 149 refused this one); transport streams are for broadcast and recording tools, set-top boxes and players such as VLC and ffplay. For playback in a browser, package the MP4 as HLS instead ([Convert MP4 to HLS](https://recursiveai.org/onemedia/cookbook/convert-mp4-to-hls/), a preview, with TS or CMAF segments).

## Limits

- **What a TS refuses.** From the README: “**TS refuses Opus and AAC whose layout is in a program config element, and writes a decode-order Matroska VFW H.264 track with PTS equal to DTS**”. Vorbis, FLAC, VP8/VP9/AV1, PCM, ALAC, AMR and WMA are refused too ([MPEG-TS muxer](https://recursiveai.org/onemedia/docs/muxing-ts/)); a refused track is listed in the report, and `LostMediaTracks` names it when it was video or all of the audio.
- **One program.** Captions inside the video, teletext, DVB subtitles, timed ID3 and SCTE 35 are carried; tags, dispositions, multi-edit edit lists and rotation have no TS form and are listed as not carried.
- **No re-encoding.** To go the other way, `Media.Remux("in.ts", "out.mp4")` re-frames the TS’s Annex B H.264 and ADTS AAC for MP4 ([Read MPEG-TS info](https://recursiveai.org/onemedia/cookbook/read-mpeg-ts-info/)).
