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.
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:
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; }
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:
ffprobe -v trace sintel-faststart.mp4 2>&1 | grep -o "type:'[a-z]*' parent:'root'"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.
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; done8484b8744e69f9447fd796647c4d56ef - 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
ilocitem 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.Remuxwrites moov first by default, but it rebuilds the sample tables and drops unknown boxes;Faststartkeeps 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.