All posts
FFmpegEngineering12 Sep 2026 · 4 min read

Six ways an MP4 can be quietly broken

JPJean Perez

An MP4 that plays fine in one player and fails in another is common. The format has no single checksum that says a file is valid. It has a container structure a decoder trusts to be internally consistent, and each player tolerates inconsistencies differently: one skips a bad sample and keeps going, another gives up on the whole file.

The container is a tree of boxes, sometimes called atoms: each one a type and a length, nested inside each other. ftyp says what kind of file this claims to be. moov holds the metadata: track list, sample tables, timing. mdat holds the raw encoded samples the tracks point into. Checking whether an MP4 is sound means walking that tree and checking whether what moov claims about the samples actually matches what's in mdat, which tells you more than whether the file happens to open.

What the six checks test

CheckWhat it's testingWhat breaks if it fails
moov after mdatWhether the index comes before or after the dataA player without the whole file can't start decoding until it has read past all the sample data to reach the index
Samples past end of fileWhether the sample table's offsets and sizes stay inside the fileA decoder seeks or reads past EOF for the last samples it's told exist
No SPS or PPS in an H.264 trackWhether the parameter sets a decoder needs before it can decode anything are present and parseThe track is undecodable from frame one, independent of any individual sample being fine
A/V start driftWhether the leading edit list offsets video and audio to the same start pointPicture and sound are out of sync from the first frame, because the container tells the player to start them at different times
Decreasing DTS or sample-count mismatchWhether decode order is non-decreasing and the sample table's own counts agree with itselfA decoder fed samples out of order, or a table that promises more samples than it delivers
NAL length prefixes that don't span their sampleWhether each length-prefixed NAL unit inside a sample actually accounts for the sample's declared sizeA sample that looks complete at the container level is truncated or malformed inside

Two of these are worth a closer look, because at first glance they don't look like corruption.

Parameter sets live outside the samples

H.264 doesn't encode a frame that fully describes itself. A decoder needs a Sequence Parameter Set and a Picture Parameter Set first, small structures that carry the things every frame in the sequence depends on: resolution, profile, how reference frames are managed. In an MP4, those live once in the track's avcC box, apart from the sample data.

A file can have every sample present, every offset correct, every timestamp in order, and still be undecodable, because the SPS or PPS in avcC doesn't parse. That's a different failure from a missing or truncated sample. Every frame is present, and none of them can be decoded.

NAL length prefixes are their own layer of truth

Inside each sample, an H.264 track stores its NAL units length-prefixed: four bytes (typically) giving the length of the unit that follows, then that many bytes of unit, repeated until the sample's declared size is used up. That's a second, independent claim about size sitting inside a claim the container already made once, at the sample-table level.

Checking whether those two agree, whether walking the length-prefixed units inside a sample actually lands exactly on the sample's declared end, catches truncation and malformed encodes that the outer sample table alone can't see. The container can be perfectly self-consistent about where each sample starts and ends. The bytes inside that boundary can still be garbage.

Skipped and failed are different results

QuickTime's .mov files share MP4's box format but use it more liberally. A hdlr box, which declares what kind of data a track holds (video, sound, or something else), normally appears once per track. A .mov file can carry a second hdlr nested under minf, deeper in the same track's box tree, and a parser that walks boxes by type without tracking where it is in the hierarchy will read the second one and silently overwrite the first, mistyping a perfectly ordinary video or audio track as generic data.

Once a track is mistyped, every check downstream of it is answering the wrong question. Looking for an SPS or PPS inside a track the parser thinks holds arbitrary data was never a check that ran at all. Reporting that as a pass would be wrong, because nothing was verified. Reporting it as a plain fail would be just as wrong, because there's no evidence the parameter set is actually missing.

That's the distinction a checker like this has to hold onto: absence of evidence and evidence of absence are different findings, and collapsing them into one bit loses the thing the check exists to report in the first place. A result of "skip" says the file wasn't readable enough to check. A result of "fail" says it was checked and it's wrong. Only one of those is actually about whether the MP4 is broken.