Recursive AI

Create a DASH MPD from MP4 in C#

All onemedia recipes

Media.PackageDash("in.mp4", "dash") cuts a video into CMAF segments, one directory per track, and writes the DASH manifest, manifest.mpd, that describes them. Nothing is re-encoded: the segments carry the MP4’s own coded pictures and audio frames. Packaging is new in 0.7.0-preview.1 and is a preview.

A preview that no validator has passed. In the README’s words:

  • “No validator has passed the output. Apple mediastreamvalidator was not run (it needs an Apple ID sign-in). The DASH-IF conformance tool v3.1.0 ran once (two findings, both fixed) and then fails in its own base-URI handling on every run.”
  • “What did check it: packet equality with the input through ffprobe (28 CMAF and 19 transport-stream inputs, 0 unexplained), TSDuck continuity on 38 of 38 transport-stream segment sets, and playback in hls.js 1.7.4 and dash.js 5.2.1 in Chrome 155, Firefox 141 and Playwright WebKit 26 (one failure: HEVC in transport-stream segments through hls.js in Chrome, not investigated) plus native HLS in macOS WebKit (7 of 7).”
Terminal
dotnet new console -o Mp4ToDash && cd Mp4ToDash
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): 52 seconds of H.264 video and AAC audio. Put this in Program.cs and run dotnet run:

C#
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Streaming;
 
// CMAF segments of about six seconds, cut on keyframes, and a DASH manifest.
PackagingReport report = Media.PackageDash("sintel.mp4", "dash");
 
foreach (PackagedTrack track in report.Tracks)
    Console.WriteLine($"{track.Name}: {track.Kind} {track.Codecs}, {track.Segments} segments, " +
                      $"{track.Bytes:N0} bytes, {track.DurationSeconds:0.###} s");
foreach (string manifest in report.Manifests)
    Console.WriteLine($"wrote {manifest}");
foreach (var loss in report.Losses)
    Console.WriteLine($"not carried: {loss.Kind} {loss.Subject}: {loss.Reason}");
foreach (string note in report.Notes)
    Console.WriteLine($"note: {note}");
Text
video0: Video avc1.64001E, 6 segments, 3,525,834 bytes, 52.208 s
audio0: Audio mp4a.40.2, 6 segments, 842,751 bytes, 51.947 s
wrote manifest.mpd

The manifest

Terminal
cat dash/manifest.mpd
Text
<?xml version="1.0" encoding="utf-8"?>
<MPD type="static" profiles="urn:mpeg:dash:profile:isoff-live:2011" minBufferTime="PT2S" mediaPresentationDuration="PT52.208334S" maxSegmentDuration="PT10.432S" xmlns="urn:mpeg:dash:schema:mpd:2011">
  <Period id="0" start="PT0S">
    <AdaptationSet id="0" contentType="video" segmentAlignment="true" startWithSAP="1" mimeType="video/mp4">
      <Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
      <Representation id="video0" codecs="avc1.64001E" width="854" height="480" bandwidth="1080650">
        <SegmentTemplate timescale="24" startNumber="1" media="video0/seg-$Number%05d$.m4s" initialization="video0/init.mp4">
          <SegmentTimeline>
            <S t="0" d="224" />
            <S d="172" />
            <S d="175" />
            <S d="201" />
            <S d="250" />
            <S d="231" />
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
    </AdaptationSet>
    <AdaptationSet id="1" contentType="audio" segmentAlignment="true" startWithSAP="1" mimeType="audio/mp4">
      <Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
      <Representation id="audio0" codecs="mp4a.40.2" audioSamplingRate="48000" bandwidth="132045">
        <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
        <SegmentTemplate timescale="48000" startNumber="1" media="audio0/seg-$Number%05d$.m4s" initialization="audio0/init.mp4">
          <SegmentTimeline>
            <S t="0" d="448512" />
            <S d="344064" />
            <S d="350208" />
            <S d="401408" />
            <S d="500736" />
            <S d="448512" />
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

A static manifest in the DASH live profile, which is the one for segment templates: each representation names its init.mp4 and its numbered segments, and the SegmentTimeline lists each segment’s length in the track’s own timescale (224 of 24 is the first video segment’s 9.33 s). A segment ends at the first keyframe at or after six seconds. The files:

Terminal
find dash -type f | sort | tr '\n' ' ' | fold -s -w 100
Text
dash/audio0/init.mp4 dash/audio0/seg-00001.m4s dash/audio0/seg-00002.m4s dash/audio0/seg-00003.m4s
dash/audio0/seg-00004.m4s dash/audio0/seg-00005.m4s dash/audio0/seg-00006.m4s dash/manifest.mpd
dash/video0/init.mp4 dash/video0/seg-00001.m4s dash/video0/seg-00002.m4s dash/video0/seg-00003.m4s
dash/video0/seg-00004.m4s dash/video0/seg-00005.m4s dash/video0/seg-00006.m4s

PackagingOptions.SingleFile writes one media.mp4 per track instead, with a sidx index, and the manifest uses the on-demand profile with byte ranges. To write HLS playlists for the same segments, call Packager.Package with Formats = PackagingFormats.Both, or see Convert MP4 to HLS.

Check it with ffprobe

ffprobe reads the manifest itself (give it an absolute path: its DASH reader fails on dash/manifest.mpd). Every packet of both tracks has the time, keyframe flag, size and bytes (MD5) it has in the MP4:

Terminal
for s in v:0 a:0; do
  for f in sintel.mp4 "$PWD/dash/manifest.mpd"; do
    ffprobe -v error -select_streams $s -show_entries packet=pts_time,flags,size,data_hash -show_data_hash MD5 -of csv=p=0 $f > $(basename $f).csv
  done
  cmp -s sintel.mp4.csv manifest.mpd.csv && echo "$s: $(wc -l < manifest.mpd.csv) packets equal to the MP4's"
done
Text
v:0: 1253 packets equal to the MP4's
a:0: 2435 packets equal to the MP4's

And one segment, the fourth video segment after its initialization segment:

Terminal
cat dash/video0/init.mp4 dash/video0/seg-00004.m4s > seg4.mp4
ffprobe -v error -count_packets -show_entries stream=codec_name,width,height,nb_read_packets -of compact seg4.mp4
ffprobe -v error -show_entries packet=pts_time,flags -of compact seg4.mp4 | head -n 2
Text
stream|codec_name=h264|width=854|height=480|nb_read_packets=201
packet|pts_time=23.791667|flags=K__
packet|pts_time=23.875000|flags=___

It starts at 23.79 s, where the timeline’s first three segments end (224 + 172 + 175 = 571 of 24), on a keyframe.

Play it in a browser

Serve the dash folder from any web server and give dash.js the manifest:

HTML
<video id="video" controls muted></video>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/modern/umd/dash.all.min.js"></script>
<script>
  dashjs.MediaPlayer().create().initialize(document.getElementById("video"), "dash/manifest.mpd", true);
</script>

Served from a local web server, dash.js 5.2.1 in headless Chrome 149 played this output with no errors: 854 × 480, duration 52.208 s, readyState 4, the time advancing from 0.50 s to 2.50 s over two seconds, then a seek to 30 s that landed at 30.000 and went on playing (31.96 s two seconds later).

From the command line, onemedia package sintel.mp4 dash --dash does the same (dotnet tool install --global RecursiveAI.OneMedia.Tool --version 0.7.0-preview.1).

Limits

  • A preview that no validator has passed. The project verifies DASH “by the packet differential and by dash.js playback, not by the conformance tool. This is a known gap.” (Packaging).
  • No encryption. From the README: “encryption is not written; live mode takes one input and holds the current segment in memory”.
  • No re-encoding. Segments can only start on the input’s keyframes. A bit-rate ladder needs inputs already encoded at each rate (Packager.Package(new[] { "1080p.mp4", "720p.mp4" }, "dash")), and renditions switch cleanly only when their keyframes coincide.
  • CMAF only. DASH is written with CMAF segments; transport-stream segments are for HLS.
  • Reading DASH back. Media.Open("dash/manifest.mpd") reads the presentation as one file (Read an HLS stream, which works the same for an MPD). From the README: “live DASH is not reloaded”.