MP4 MP4 Analyzer by MediaOnDeck

// engineering · methodology

Building motion-vector ground truth with ffprobe

When some frames appeared to have "no motion vectors", the first instinct was to suspect the locally patched FFmpeg build. The right next step was not to open the decoder source, but to generate native ground truth — one command, thirty seconds, case closed.

1. Symptom: a batch of frames with "no motion vectors"

Block analysis exports the motion vectors (MV) of every frame. On a 1634 MB H.264 clip, a number of frames in the export carried almost no arrows at all.

The suspicion was reasonable: block analysis runs on a locally patched, locally rebuilt FFmpeg (the patch is what exports MV and QP to the JavaScript layer), so "our change broke something" is a natural hypothesis. Following it, though, means reading decoder source to check how MV side data is populated — hours of work that would very likely have reached the same conclusion as below.

So we did the other thing first: produce output that does not depend on any of our code.

2. One command builds ground truth

ffprobe -export_side_data mvs -show_frames input.mp4

-export_side_data mvs makes ffprobe attach the motion vectors obtained during decoding to each frame as side data. The output is enormous, so trim it with -show_entries:

ffprobe -v error -select_streams v:0 \
  -export_side_data mvs \
  -show_entries frame=pkt_pts_time,key_frame:frame_side_data \
  -of csv=p=0 input.mp4

The result:

ClipRangeFrames with no MV
1634 MB H.264 First 250 frames Frame 0 (an I-frame) and frame 150

Our export matched it frame for frame — including those two. No bug. The whole investigation took about thirty seconds.

3. Why frames legitimately can have no motion vectors

This was the most valuable outcome: "this frame has no motion vectors" is a legitimate result, not an error.

  • Intra-coded frames (I-frames). They do not participate in inter prediction at all, so they have no motion vectors. Frame 0 above is one of these.
  • Nominally P or B frames where every block chose intra prediction. A typical trigger is a scene cut: the new picture has nothing to match against, and the encoder decides redrawing the whole frame is cheaper than finding a match. Frame 150 is one of these — whether it was a scene cut or another encoder decision cannot be told from the missing MVs alone; you would look at the prediction-mode distribution in that frame's slices.

Which retires an assumption worth crossing out: "if the frame type is P or B, it must have motion vectors" is false. Any check built on it will misfire at scene cuts.

Inverted: a P or B frame with no MVs at all is worth a look, but it is not an error. What genuinely deserves an error is a structural contradiction — for example a frame that has MVs whose reference index points at a frame that does not exist.

4. A sibling technique: display order ≠ decode order

Another recurring trap involves ffprobe, just not the MV side of it: the presence of B-frames means decode order differs from display order.

When extracting a single frame from ffmpeg for a comparison, the K in select='eq(n,K)' is a decode / output order index, not the frame number you see in a tool's interface. Feeding in the interface number extracts a different frame — and then you start doubting your own decoder.

Look it up by packet offset instead:

# build a "display order → packet offset" table
ffprobe -v error -select_streams v:0 \
  -show_entries frame=pkt_pos -of csv=p=0 input.mp4

# then find the target sample's absolute byte offset in that table
# → that index is what goes into select='eq(n\,index)'

Two measured examples, both far beyond "off by one":

ClipsampleIndex in the toolDisplay index in ffmpeg
Clip A3332
Clip B135269

A gap of 134 frames. Guessing POC, or "roughly a few frames off", does not work — resolve it by byte offset.

Any such criterion also needs a reverse control to show it discriminates. Compare the exported picture against ffmpeg's neighbouring display-order frame; if the criterion works, the correct frame should be clearly closer:

Compared againstMean diffMax diff
The correct frame 0.14 1
Neighbouring display-order frame 3.17 – 7.83 255

Two orders of magnitude apart, so the criterion is trustworthy. Otherwise "it matches" may just mean "any two nearby frames look alike".

5. Two small traps that cost half an hour each

  • Trailing commas. Lines from ffprobe -of csv=p=0 can end with a comma (for instance 44,). Number('44,') is NaN, the record gets silently filtered out, and you conclude "that offset isn't in ffprobe's output at all" — then start doubting your frame-index conversion. Split on the comma before parsing.
  • maxBuffer. On a 155,000-frame track, ffprobe's output exceeds Node execFileSync's default 1 MB buffer and throws ENOBUFS. This has nothing to do with the file under analysis; it is purely a test-harness default. Raise maxBuffer.

Both traps share a shape: the symptom looks like "the thing under test is broken", when in fact "the measuring tool is misconfigured". Treating every link in the measurement chain as suspect is the cheapest discipline available in this kind of investigation.

6. Methodology: build ground truth before asserting "this is our bug"

PathCostOutcome
Read FFmpeg decoder source for MV population Hours Probably the same conclusion
Build native ground truth with ffprobe ≈ 30 s Definitive

So it became a standing procedure:

  1. Ask first whether a native tool can produce an equivalent quantity. If it can, generate one and compare. Only if it cannot should reading source be on the table.
  2. Ground truth must not come from the code being verified. Producing and validating with the same implementation verifies nothing.
  3. The conclusion must distinguish "we got it wrong" from "it is like that anyway". This case was the latter, and it looked exactly like the former.

The procedure has a precondition: the native tool must genuinely be equivalent. Motion vectors happen to have an exporter (mvs); QP does not, so that side still relies on other means — comparing intra/inter block statistics against the native decoder's output rather than diffing numbers directly.

7. Transferable lessons

  • When an existing tool can give you ground truth, do not read source. Source reading is the last resort, not the first reflex.
  • When a quantity comes back empty, ask whether empty is legitimate. The question costs nothing and can save an entire wrong-direction investigation.
  • Resolve frame identity by byte offset, never by guessing order. The gap between decode and display order can be over a hundred frames.
  • Validate the criterion itself. Run a reverse control showing the correct frame is genuinely closer than its neighbour, otherwise "it matches" carries no information.

Related: 623 keyframes, zero detected · verifying pixel export on YUV planes · FAQ · Open the analyzer