MP4 MP4 Analyzer by MediaOnDeck

// engineering · performance

Moving 92% of a file to read four bytes

Opening a 1.7 GB film, all we needed was each frame's I / P / B type. Doing so moved 1506 MB — 92% of the file — plus about 155,000 allocations, in 6.9 seconds. After the rework: 0.84% (13.7 MB) and 460 ms, with byte-identical frame types.

1. The symptom: opening a file takes seconds

The file is local, and all we produce is a frame table — yet it took 6.9 seconds. "Reading metadata is slow" almost never means the algorithm is expensive; it means you are reading too much.

Frame-type detection used to locate the sample's start and length, then read the whole sample, purely to parse a slice header and pick out one field: slice_type.

MetricBeforeAfter
Bytes read1506 MB (92%)13.7 MB (0.84%)
Analysis time≈ 6.9 s≈ 460 ms
Allocations≈ 155,000Sharply down
Frame typesByte-for-byte identical

2. Measure first, then judge

The valuable part here is not the optimisation but how we first got it wrong.

The reasoning was: "the media data box (mdat) is not a container box — it holds no child boxes — so walking the box tree never touches media data, and therefore this path does not read large files."

That sounds entirely reasonable, and the conclusion was wrong. "Walking the box tree does not touch media data" and "the whole pipeline never touches media data" are two different claims. Frame type detection is a separate path: it seeks by sample offset and knows nothing about the box tree.

The right first move is a counter: accumulate bytes at every point where this path actually reads, run it on a large file, print the number. The moment 92% appeared, the direction was settled. Do not infer I/O volume from data structures — measure it.

3. The approach: a growing window

Very few bytes are actually needed: parsing a slice header usually takes a few dozen. So the read became a grow-on-demand window instead of a full read:

window start = sample start
window size  = 64 B

loop:
  scan the NAL chain inside the window
  did we find the NAL that carries picture data (a slice NAL)?
     └─ yes → parse the slice header in the current window; done
     └─ no  → window size ×4, retry (with an attempt cap)

Parsing can proceed even from a truncated window because everything needed lives at the very start of the NAL. So the window bound is min(end of NAL, end of data read) with an extra 8-byte margin.

4. Two boundaries that are easy to get wrong

CaseCorrect behaviourConsequence of getting it wrong
The window truncates a slice NAL Parse it as usual (only the start is needed) None — this truncation is harmless
The window truncates a non-slice NAL Must grow the window and retry A slice NAL later in the sample is missed → wrong frame type

The second row is the trap actually hit here. Samples beginning with SEI are common ([SEI, SEI, IDR]). If the window is cut in the middle of that SEI and the code does not grow and retry, it concludes "this sample contains no slice" — the frame type goes wrong, and it looks entirely normal.

The distinction is clean: a truncated slice means "read enough"; a truncated non-slice means "not finished reading". The two must take different branches.

5. Verify by full diff, not by sampling

Changing how bytes are read always raises the question "did the results change?". The answer here came from a frame-by-frame, byte-by-byte diff: export the entire frame-type sequence before and after, and compare all of it.

The requirements:

  • material must cover H.264 and H.265 (their slice headers differ);
  • and both large and small files (small files expose the window-growth boundaries);
  • the comparison is the full frame-type sequence, not "a few frames look fine".

The reason is the nature of this change: it is supposed to be an equivalent transform (the bytes no longer read were never used). But "supposed to be equivalent" has to be demonstrated — and one full diff is far cheaper than later chasing "frame types are wrong on some material".

6. Transferable lessons

  • Measure I/O, never infer it. Reasoning about "does this touch large files" from data structures produced exactly the wrong answer. A byte counter and one large file beat any amount of inference.
  • To stream something, find the minimum window first. Most parsing needs only the first few dozen bytes of a structure. Establish "how little do we need" before deciding how much to read — not the other way round.
  • Treat truncation as a first-class case. Separate "read enough, but truncated" from "not finished reading". Conflating them produces silent errors.
  • Equivalent transforms need full proof. "The bytes we skip are unused" is good enough for design, not for delivery. One full diff turns reasoning into fact.

Related: Fitting a GB-scale file into a 2 GB heap · 623 keyframes, zero detected · How it works · Open the analyzer