Recursive AI

MP4 faststart in C#: move the moov atom to the front

All onemedia recipes

An MP4 whose movie box (moov, the index) sits after the media data (mdat) can’t start playing in a browser until the end of the file has arrived. Media.Faststart writes a copy with moov in front. It is an edit, not a remux: the media data and every other box are copied as they are, and only the chunk offset tables change. The input is the Sintel trailer (CC BY 3.0, © copyright Blender Foundation | durian.blender.org), whose moov is at the end.

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

Put this in Program.cs and run dotnet run:

C#
using System.Buffers.Binary;
using System.Text;
using RecursiveAI.OneMedia;
using RecursiveAI.OneMedia.Remuxing;
 
Console.WriteLine("before: " + string.Join(" ", TopLevelBoxes("sintel.mp4")));
 
FaststartReport report = Media.Faststart("sintel.mp4", "sintel-faststart.mp4");
 
Console.WriteLine("after:  " + string.Join(" ", TopLevelBoxes("sintel-faststart.mp4")));
Console.WriteLine($"moved: {report.Relocated}, moov {report.MovieBoxBytes:N0} bytes, "
                  + $"{report.OffsetsShifted:N0} chunk offsets shifted, {report.BytesCopied:N0} bytes copied");
 
// The top-level boxes of an MP4: a 4-byte size and a 4-byte type each (size 1: a 64-bit size follows).
static List<string> TopLevelBoxes(string path)
{
    var boxes = new List<string>();
    using FileStream file = File.OpenRead(path);
    byte[] header = new byte[16];
    while (file.Position + 8 <= file.Length)
    {
        long start = file.Position;
        file.ReadExactly(header, 0, 8);
        long size = BinaryPrimitives.ReadUInt32BigEndian(header);
        if (size == 1) { file.ReadExactly(header, 8, 8); size = (long)BinaryPrimitives.ReadUInt64BigEndian(header.AsSpan(8)); }
        else if (size == 0) size = file.Length - start;
        boxes.Add(Encoding.ASCII.GetString(header, 4, 4));
        file.Position = start + size;
    }
    return boxes;
}
Text
before: ftyp free mdat moov
after:  ftyp moov free mdat
moved: True, moov 39,456 bytes, 3,688 chunk offsets shifted, 4,332,917 bytes copied

moov now follows ftyp; free and mdat keep their order behind it. Every chunk offset that pointed into mdat moved forward by the size of moov. A table whose offsets no longer fit 32 bits is promoted to co64 (report.PromotedTables). A file that is already moov-first is copied unchanged and Relocated is false.

Check it with ffprobe

FFmpeg’s trace shows the same box order:

Terminal
ffprobe -v trace sintel-faststart.mp4 2>&1 | grep -o "type:'[a-z]*' parent:'root'"
Text
type:'ftyp' parent:'root'
type:'moov' parent:'root'
type:'free' parent:'root'
type:'mdat' parent:'root'

The packets are the same as the original’s, times and flags included: ffprobe lists both files’ packets with the same timestamps, sizes, flags and payload hashes.

Terminal
for f in sintel.mp4 sintel-faststart.mp4; do ffprobe -v error -show_entries packet=stream_index,pts,dts,size,flags -show_data_hash MD5 -of csv $f | md5sum; done
Text
8484b8744e69f9447fd796647c4d56ef  -
8484b8744e69f9447fd796647c4d56ef  -

It still plays: in headless Chrome 149 on Linux, sintel-faststart.mp4 loaded from a local HTTP server reached readyState 4 with a duration of 52.208 s, played past 2.5 s, and decoded video frames and audible audio.

Limits

  • Progressive MP4, MOV and 3GP only. Fragmented MP4 has no single movie box and is refused.
  • Offsets it doesn’t adjust. HEIF-style iloc item locations and vendor boxes that hold absolute file offsets keep their old values.
  • Memory. The movie box is held in memory (bounded by MediaLimits.MaxAllocationBytes); the media data streams through a 1 MiB buffer whatever the file size.
  • A new file. The output must not exist. To get moov-first output while also changing the container or tracks, Media.Remux writes moov first by default, but it rebuilds the sample tables and drops unknown boxes; Faststart keeps the file as it is. Details: Remux and faststart.

To check whether a browser profile plays the result, see Check if a browser can play a video.