# Parsing ffprobe output is harder than it looks

Canonical: https://rendobar.com/blog/ffprobe-metadata-gotchas/
Author: Abdelrahman Essawy
Published: 2026-07-19
Updated: 2026-08-20

---

## Key takeaways

- ffprobe reports rotation in a stream's display-matrix side data, not the old rotate tag, so a phone video that plays upright can read as 1080x1920.
- The image2 demuxer gives a still image a fake 0.04 second duration and a huge bitrate. Both are meaningless.
- A WAV file can report channels: 2 with no channel_layout at all, so 'stereo' has to be inferred.
- Cover art shows up as a video stream (disposition.attached_pic=1), so an MP3 looks like it has video.
- HDR isn't a boolean. You read it from color_transfer (smpte2084 or arib-std-b67) or color_primaries (bt2020).

`ffprobe` is the first thing I reach for when a video does something weird. One command and you get the codec, the resolution, the duration, and every stream, all as JSON. It's the best tool of its kind.

Then you start trusting that JSON literally, and it bites you.

I spent a while building a normalizer for ffprobe output at Rendobar. These are the fields I stopped trusting, and why.

## Rotation isn't where you'd look

A video shot in portrait on a phone usually stores its frames in landscape and carries a rotation. Older files put that in `tags.rotate`. Newer files, and ffmpeg 8 in particular, put it in the stream's `side_data_list` as a Display Matrix and drop the tag entirely. So if you read `tags.rotate`, a phone clip that plays perfectly upright reports `1080x1920` and you compute the wrong aspect ratio. You have to read the display matrix and normalize it to 0, 90, 180, or 270.

## Images have a fake duration

Run ffprobe on a JPEG and the `image2` demuxer hands you `"duration": "0.040000"` and a bitrate in the millions. That is not the image. It's the demuxer pretending the still is one frame of a 25 fps video. A PNG comes through `png_pipe` with no duration at all. If you surface that 0.04 anywhere, it's wrong. Null it out.

## Channel layout goes missing

You'd expect a stereo file to say `"channel_layout": "stereo"`. A WAV often doesn't. It reports `"channels": 2` and no layout string at all. So "stereo" is something you infer from the channel count, not something you can read off the field.

## Cover art is a "video stream"

An MP3 with embedded album art has two streams: the audio, and a tiny video stream for the picture. If you count video streams to decide "is this a video," every MP3 with a cover looks like one. The tell is `disposition.attached_pic = 1`. Exclude those before you decide what kind of file you're holding.

## HDR isn't a flag

There is no `"hdr": true`. You work it out from the color metadata: a `color_transfer` of `smpte2084` (HDR10) or `arib-std-b67` (HLG), or `color_primaries` of `bt2020`. Miss it and you hand an HDR clip to a pipeline that treats it as SDR.

## The short version

None of this is ffprobe being wrong. It's ffprobe being literal, reporting exactly what the container claims even when the container is quietly lying. The real work is turning that into something you can act on.

That's the work we did once so you don't have to. Rendobar's [ffprobe job](https://rendobar.com/docs/jobs/ffprobe) runs the real ffprobe command, either a full command or just a URL, and hands back a normalized summary with rotation, HDR, variable frame rate, and the rest already sorted out, next to the raw report when you want it.

## The rest of the ffprobe cluster

[Common ffprobe commands compared](/blog/ffprobe-commands-compared/) is the
command reference, measured across 213 real probe jobs.
[How to get video duration with ffprobe](/blog/ffprobe-video-duration/) covers the
one field that disagrees with itself.
[Validate video uploads with ffprobe](/blog/ffprobe-validate-uploads/) covers using
a probe as a gate before you spend anything on an encode.
