MP4 File Documentation


Summary

An MPEG-4 Part 14 (MP4) file is a multimedia container that holds video, audio, subtitles, and metadata in one file. It is the near-universal digital video format, standardised as ISO/IEC 14496-14 with the MIME type video/mp4. Windows, macOS, phones, and every modern browser play a .mp4 file natively, so the common questions are about extracting audio (“MP4 to MP3”) and files that won’t play because of a missing codec.

Technical details

FeatureValue
Full nameMPEG-4 Part 14 (MP4)
File extension.mp4
MIME typevideo/mp4
Format typeMultimedia container, box/atom-based binary
Base formatISO Base Media File Format (ISO/IEC 14496-12)
DeveloperMoving Picture Experts Group (MPEG / ISO/IEC)
Introduced2001 (MP4 v1); Part 14 standardised 2003
StandardISO/IEC 14496-14
Open standardYes — published ISO/IEC specification
Byte orderBig-endian
Magic number (hex)66 74 79 70 (ftyp) at offset 4
Video coding formatsH.264/AVC, H.265/HEVC, AV1, MPEG-4 Part 2
Audio coding formatsAAC (usual), MP3, AC-3, ALAC
Subtitle supportYes
Chapter supportYes
Streaming supportYes (progressive and adaptive; moov-at-front for fast start)
ResolutionCodec-limited; up to 4K/8K in practice
DRM supportYes (ISO Common Encryption, e.g. Widevine, FairPlay, PlayReady)
Related extensions.m4v, .m4a, .mov, .3gp, .mkv
Specificationiso.org/standard/79110.html
File signature (magic bytes)
.. .. .. .. 66 74 79 70

The signature sits at offset 4, not 0. Bytes 0–3 are the size of the first box (often 00 00 00 18, 1C, or 20), and bytes 4–7 are 66 74 79 70, ASCII ftyp — the file-type box. The four bytes that follow are the major brand: isom, mp42, mp41, M4V , or avc1, which name the exact MP4 flavour. Because MP4, .mov, .m4a, .m4v, and .3gp all use the ISO Base Media File Format, they share this same ftyp marker at offset 4.

What is an MP4 file?

MP4 is short for MPEG-4 Part 14, standardised by the Moving Picture Experts Group as ISO/IEC 14496-14 in 2003. It is a container: a structured wrapper that stores one or more timed media tracks together with the metadata a player needs to decode and synchronise them. The container itself is defined on top of the ISO Base Media File Format (ISO/IEC 14496-12), the same base structure used by MOV, M4A, .m4v and .3gp. Its MIME type is video/mp4.

A typical .mp4 holds one H.264 or H.265 video track and one AAC audio track, plus optional subtitle, chapter and metadata tracks. None of that coded media is described byte-for-byte by MP4 itself. The container records where each sample lives and when it should be presented; the actual bitstream is the codec’s business. Everything below is about how ISO BMFF encodes that structure, box by box.

The box header: size, type, largesize and uuid

An ISO BMFF file is a flat sequence of nested boxes (the QuickTime term is “atoms”). Every box begins with the same 8-byte header, and boxes contain other boxes, so a parser can walk the whole tree without knowing any specific box in advance.

aligned(8) class Box (unsigned int(32) boxtype) {
    unsigned int(32)  size;        // bytes 0-3: total box length incl. this header
    unsigned int(32)  type;        // bytes 4-7: four-character code, e.g. 'moov'
    if (size == 1)
        unsigned int(64) largesize; // 64-bit length when size == 1
    else if (size == 0)
        ;                           // box runs to end of file
    if (type == 'uuid')
        unsigned int(8)[16] usertype; // 16-byte extended type
    // ... box payload follows ...
}

The first field is a 32-bit big-endian size giving the box’s total length including the header. The second is a 32-bit type, four printable ASCII characters such as ftyp, moov or mdat. Two escape values matter. A size of 1 signals that a 64-bit largesize follows the type, which is how an mdat box exceeds 4 GB. A size of 0 means the box extends to the end of the file, valid only for the last box. When type equals uuid, the next 16 bytes are a full UUID that names a vendor extension box, letting private data ride inside a standard file that other readers skip cleanly.

Because each box carries its own length, an unknown box is trivially skipped: read size, jump size bytes, continue. That forward-compatibility rule is why a two-decade-old parser can still open a file that uses boxes invented after it was written. All multi-byte integers in the format are big-endian (network byte order).

ftyp: brands and compatibility

The first box in a well-formed MP4 is ftyp, the file-type box, which is why the ASCII string ftyp appears at byte offset 4 rather than offset 0: bytes 0–3 are its size. Its payload declares which specifications the file conforms to.

ftyp
  major_brand        : uint32  'mp42'   // preferred brand / spec
  minor_version      : uint32  0        // informative version of major_brand
  compatible_brands[]: uint32  'isom','mp41','mp42','avc1' ...  // list to EOF

The major_brand is the single brand the file most wants to be read as, for example mp42 (MP4 v2) or isom (the ISO base profile). minor_version is an informative integer, not a strict version gate. The compatible_brands array then lists every brand the file also satisfies, one four-character code per entry, running to the end of the box. A player reads this list and, if it recognises any brand, knows it can handle the file: seeing avc1 tells it H.264 conventions apply, iso6 implies fragmented-file support, M4A  marks an audio-only M4A. The brand set is advisory about structure and profile; it does not by itself name the codecs, which are declared later in the sample descriptions.

The moov hierarchy: mvhd, trak and the track tree

The moov box is the movie box: it holds all metadata and no coded media. It contains one mvhd movie header plus one trak box per track. The nesting is deep and every level has a job.

moov
 ├─ mvhd                movie header: timescale, duration, next_track_ID
 ├─ trak                one track (e.g. video)
 │   ├─ tkhd            track header: track_ID, flags, width, height, matrix
 │   └─ mdia            media info for this track
 │       ├─ mdhd        media header: this track's timescale + duration
 │       ├─ hdlr        handler: 'vide' / 'soun' / 'subt' — what kind of track
 │       └─ minf        media information container
 │           ├─ vmhd/smhd  video or sound media header
 │           ├─ dinf        data information (where the media lives: dref)
 │           └─ stbl        the SAMPLE TABLE — see below
 └─ trak                another track (e.g. audio)

The mvhd box carries the movie-level timescale, the number of time units per second against which the overall duration is expressed. A timescale of 1000 means durations are in milliseconds; video tracks often use 90000 (90 kHz). mvhd also stores the total duration in those units and next_track_ID. Each trak opens with tkhd, the track header, holding the track’s track_ID, enabled/in-movie flags, a 3×3 transform matrix, and for video the display width and height as 16.16 fixed-point.

Inside trak, the mdia box describes the media. Its mdhd gives this track’s own timescale and duration (a track can be timed independently of the movie), and hdlr gives the handler type: vide for video, soun for audio, subt or text for subtitles. Below that, minf collects a media header (vmhd or smhd), a data-reference box dinf/dref that says whether the media is in this file or an external one, and finally stbl, where the real indexing lives.

The stbl sample tables

Everything a player needs to find, time and decode an individual sample is packed into the stbl (sample table) box as a set of parallel tables. A sample is one coded unit: one video frame, or one audio frame of AAC. The tables are stored separately so each stays compact, and a reader cross-references them to reconstruct the timeline.

stbl
 ├─ stsd   sample descriptions: codec + config per sample entry
 ├─ stts   time-to-sample: sample durations (run-length coded)
 ├─ ctts   composition time-to-sample: PTS offsets (B-frame reorder)
 ├─ stsc   sample-to-chunk: how many samples per chunk
 ├─ stsz   sample sizes (or stz2 for compact sizes)
 ├─ stco   chunk offsets into the file (or co64 for 64-bit)
 └─ stss   sync sample table: which samples are keyframes

The stsd box holds one or more sample entries, and this is where the codec is actually named. A video entry typed avc1 or hvc1 nests a codec-configuration box: avcC for H.264 (carrying the SPS and PPS parameter sets), hvcC for H.265, and for audio an esds box holding the MPEG-4 elementary-stream descriptor with the AAC configuration. Without stsd a decoder cannot be initialised.

stts maps sample number to duration using run-length entries (“N samples each lasting D units”), which is why constant-frame-rate video needs only a single entry. ctts adds a per-sample composition offset so presentation timestamps (PTS) can differ from decode timestamps (DTS); it exists because B-frames are decoded out of display order. stsc is the sample-to-chunk map: samples are grouped into chunks, and this box run-length encodes how many samples each run of chunks contains. stsz (or the packed stz2) lists the byte size of every sample, or a single size if all are equal. stco lists the file offset of each chunk; co64 is its 64-bit form for files past 4 GB. Finally stss lists which samples are sync samples (keyframes) so a player can seek to a clean random-access point. If stss is absent, every sample is a sync sample.

AtomPurpose
stsdSample descriptions: codec type and config (avcC/hvcC/esds)
sttsTime-to-sample: decode duration of each sample, run-length coded
cttsComposition offsets: PTS−DTS for reordered B-frames
stscSample-to-chunk: how many samples are packed in each chunk
stsz / stz2Sample sizes in bytes (per sample, or one uniform size)
stco / co64Chunk offsets into the file (32-bit / 64-bit)
stssSync sample table: which samples are keyframes for seeking

mdat: locating a sample by offset, size and time

The mdat box is a single opaque blob of interleaved coded samples: a run of video frames, then a run of audio frames, then more video, arranged so a player reads roughly sequentially while feeding both decoders. Nothing inside mdat is self-describing; it is meaningless without the sample tables pointing into it. This is deliberate: metadata and media are separated so the index can be read or rewritten without touching the payload.

To fetch sample n, a reader combines four tables. stsc tells it which chunk contains sample n and its position within that chunk; stco/co64 gives that chunk’s absolute file offset; the reader sums the stsz sizes of the preceding samples in the chunk to reach the sample’s start; and stsz gives the sample’s own length. That is the exact byte range to read. Its decode time comes from accumulating stts durations up to n, and its presentation time adds the ctts offset. Seeking works the same way in reverse: pick the nearest stss sync sample at or before the target time, resolve its offset, and start decoding there.

Container versus codec, technically

The .mp4 wrapper and the compressed bitstream inside it are separate layers. The container defines boxes, tracks, timing and offsets; the codec defines how pixels and audio samples are compressed into the bytes that mdat stores. MP4 commonly carries H.264/AVC (declared by an avc1 sample entry with an avcC config), H.265/HEVC (hvc1/hev1 with hvcC), or AV1 (av01 with av1C); audio is almost always AAC in an mp4a entry with an esds descriptor. The container is codec-agnostic, so the same box structure can hold any of them.

This layering is why a file that “won’t play” is rarely a broken container. The box tree parses fine; what a player lacks is a decoder for the bitstream stsd names, most often HEVC on a system without the HEVC decoder. It also explains why a remux to MOV is lossless: MOV shares the ISO BMFF box model, so the coded samples are copied unchanged and only the surrounding boxes are rewritten. The open-codec web equivalent, WebM, instead uses a different container base (a Matroska/MKV subset) around VP9 or AV1, so moving to it means a real transcode.

Faststart: moov placement and progressive download

A player cannot present anything until it has read the moov box, because that box holds the sample tables. Many encoders emit moov after mdat, at the end of the file, since final sample sizes and offsets are only known once encoding finishes. Over HTTP that forces the client to download the whole file before playback starts, because the index is at the very end.

“Faststart” (also called web-optimising) rewrites the file so moov comes before mdat. Now the client reads ftyp and moov from the first bytes, learns the full sample layout, and can begin fetching and decoding mdat ranges immediately: progressive download. The trade-off is that all chunk offsets in stco/co64 shift when moov moves ahead of the media, so the offsets must be recomputed during the rewrite, which is what a faststart pass actually does.

Fragmented MP4: moof and traf

Progressive download still assumes one contiguous file with a single up-front index. Adaptive streaming over MPEG-DASH and HLS needs something else: many independently fetchable, independently timed segments. That is fragmented MP4 (fMP4), and it changes where the timing lives.

ftyp
moov
 └─ mvex               movie extends: declares fragments will follow
     └─ trex           per-track defaults (default sample size/duration/flags)
moof                   movie fragment (one per segment)
 ├─ mfhd               sequence_number of this fragment
 └─ traf               track fragment
     ├─ tfhd           track fragment header: track_ID, base offset, defaults
     ├─ tfdt           base media DECODE time of this fragment
     └─ trun           track run: per-sample sizes, durations, flags, CT offset
mdat                   coded samples for THIS fragment only
moof / mdat            ... repeated per segment ...

In an fMP4 the initial moov carries almost no sample data. Instead it contains an mvex box whose trex children declare per-track defaults (default sample duration, size and flags) and signal that the movie continues in fragments. The real timing arrives fragment by fragment. Each fragment is a moof box followed by its own mdat. The moof holds an mfhd with the fragment’s sequence_number, then one traf per track. Inside traf, tfhd sets the track ID and base data offset and overrides defaults; tfdt gives the absolute base media decode time so a client can place the fragment on the timeline without having seen earlier fragments; and trun (track run) lists each sample’s size, duration, flags and composition offset for the samples in that fragment’s mdat.

Because each moof + mdat pair is self-contained and self-timed, a DASH or HLS client can request just the segments it wants, at whatever bitrate, and switch renditions at fragment boundaries. The same fragmented structure, delivered in short segments, is what a browser’s Media Source Extensions feed to play adaptively without ever downloading a whole monolithic file.

References