MP4 MP4 Analyzer by MediaOnDeck

// engineering · bitstream

One skipped bit field shifted the next 32 bits

The parsed chroma format came out as 655392. The only legal values are 0 to 3. A number that far out of range almost always means the same thing: the read position is off, and you are reading someone else's bits. One segment of a parameter-set structure had been skipped, shifting every field after it by 32 bits.

1. The symptom: a value that cannot exist

Chroma format lives in 0..3 (monochrome / 4:2:0 / 4:2:2 / 4:4:4). Reading 655392 does not mean the bit reader is broken — it faithfully returned a number. It means the starting position was wrong.

Why does nothing complain? Because almost every field in an H.264 / H.265 parameter set is variable-length (Exp-Golomb). Exp-Golomb has an awkward property: it will return a value from any bit position. It never reports "illegal". So a misalignment does not throw — it simply keeps going, and the first wildly wrong value usually appears far away from the actual mistake.

2. The root cause: a structure assembled from three parts

H.265's profile_tier_level (7.3.3 in the spec) is not a flat field table. It is three concatenated parts:

PartContentsPresent when
① Flag group sub_layer_profile_present_flag[i] and sub_layer_level_present_flag[i], one bit each per sub-layer maxNumSubLayersMinus1 > 0
② Reserved group reserved_zero_2bits, repeated 8 − maxNumSubLayersMinus1 times maxNumSubLayersMinus1 > 0
③ Per-sub-layer body An 88-bit profile field set plus an 8-bit level, gated by the flags in ① Always

Skipping part ② means missing 8 − maxNumSubLayersMinus1 reserved groups. Note the number carefully: when maxNumSubLayersMinus1 = 0, part ② does not exist at all.

That is the nastiest part of this bug: the clips we happened to test with were all single-temporal-sublayer, so "one part missing" was invisible during development. Feed it a clip with several temporal sub-layers and everything shifts by 32 bits immediately.

The same family of mistake has a second doorway: after a 16-bit reserved field in the video parameter set comes a complete profile_tier_level, not a shortened form. Assuming you can skip a segment there fails exactly the same way.

3. Same family: a loop that read only sub-layer zero

Another field, *_sub_layer_ordering_info_present_flag, being 1 means you must read every sub-layer (i = 0 .. maxNumSubLayersMinus1). Ours was pinned to "read sub-layer 0, then break".

Identical symptom: no error, just misalignment. The difference is that this time the shift happens one sub-layer later — which is why it is equally invisible on single-sub-layer test clips.

4. The subtlest one: fields that depend on context

The short-term reference picture set has two syntax elements whose presence is contextual:

  • inter_ref_pic_set_prediction_flag exists only for a non-first reference set (stRpsIdx != 0);
  • delta_idx_minus1 exists only when the set is written inline in a slice header — it is not there when the set lives in the parameter set.

Once inside the prediction branch you must also consume used_by_curr_pic_flag[j] for each entry (only when it is 0 do you continue to use_delta_flag[j]), and the upper bound for j is the full count of negative plus positive pictures in the referenced set.

Miss any one of those three and the whole slice header shifts. The price: frame type, reference lists and POC all go wrong — with nothing in the pipeline raising an error.

5. Why your own parser can never be its own ground truth

The most valuable lesson here is not a field but the verification method. We originally validated the parser with the parser's own output. With the reserved group skipped, chroma format came out as 655392, but the reference lists "looked" plausible and the regression suite was green.

The fix is an independent source of truth. One command is enough:

ffmpeg -i input.mp4 -c:v copy -bsf:v trace_headers -f null -

That bitstream filter prints the parse element by element, including each field's value and position. Align your parser's output against it field by field and the first divergence is the missing read — no need to work through the spec text.

Ground truthQuestions it can answer
Element-by-element dump Parameter set / slice header misalignment — pinpoints the exact bit where you diverge
Native decoder "Is the result correct" questions: pixels, reference lists
Your own parser Regression only: proves a change did not break earlier findings. It cannot prove a finding is right

6. How to get there faster next time

  1. Hunt for impossible values first. A field whose legal domain is 0–3 reading 655392 is an alignment bug, not an arithmetic bug. Suspect the origin, not the formula.
  2. Count consumed bits. Print the cumulative bit offset after every field and diff it against independent ground truth. The first divergence is the missing read — far faster than guessing field by field.
  3. Cover the configurations that make a part vanish. Here maxNumSubLayersMinus1 = 0 hid the entire reserved group. Any spec field written as "present when X" deserves a clip that deliberately fails X.
  4. Assert on domains instead of relying on parser errors. A variable-length reader returns numbers even past the end of the data, so your own validity assertions are the only early warning you get.

7. Cost and boundaries

Parameter sets are now read from the out-of-band copy in the container (avcC / hvcC) first, falling back to scanning samples only when that fails — and the fallback is capped, so one malformed file cannot stall loading.

One last warning: the visible symptom of this class of bug is usually not in the parsing layer. It shows up as "block analysis comes back empty", "the reference list has one entry", or "POC jumps around". Chase the error message alone and you will look in the wrong place.

Related: 623 keyframes, zero detected · 269 of 271 frames had the wrong reference list · How it works · Open the analyzer