Ffmpeg Probe
Context: $ARGUMENTS
Quick start
- Everything, JSON:
ffprobe -v error -show_format -show_streams -of json in.mp4→ Step 1 - Single field (duration / fps / resolution / codec): → Step 3, one-liners
- Keyframes / chapters / metadata: → Step 1 (pick section)
- HDR detection: → Step 3, HDR recipe
- Python helper w/ subcommands:
uv run ${CLAUDE_SKILL_DIR}/scripts/probe.py summary --input in.mp4
When to use
- User says "inspect", "what codec is this", "how long", "what resolution", "is it HDR", "get the bitrate", "dump metadata", "list tracks".
- You need machine-readable media info for a downstream step (e.g. before transcoding, cutting, or streaming).
- For playback/preview use
ffmpeg-playback; for transcoding useffmpeg-transcode; for raw extradata/bitstream inspection useffmpeg-bitstream.
Step 1 — Pick what to dump
ffprobe organises output into sections. Combine as many -show_* flags as needed:
| Flag | Contents | Size |
|---|---|---|
-show_format |
Container: filename, duration, size, bit_rate, tags | small |
-show_streams |
Per-stream: codec, resolution, fps, channels, color, tags | small |
-show_chapters |
Chapter list with start/end times and titles | small |
-show_programs |
MPEG-TS programs (broadcast / multi-program streams) | small |
-show_frames |
Every decoded frame (pict_type, pts, key_frame, …) |
HUGE |
-show_packets |
Every packet (pts, dts, size, flags) |
HUGE |
-show_entries K=V,… |
Whitelist specific sections/fields (see references/queries.md) |
small |
-show_error |
Structured error if demuxing fails | tiny |
For -show_frames / -show_packets, always combine with either
-select_streams and/or -read_intervals to bound the output:
ffprobe -v error -select_streams v:0 -read_intervals "%+#20" -show_frames -of json in.mp4
# reads the first 20 packets of v:0 (the `%+#N` form = first N packets)
Step 2 — Pick output format
-of value |
Use case |
|---|---|
default |
Human-ish, section.key=value lines |
default=noprint_wrappers=1:nokey=1 |
Bare value(s), one per line — great for shell |
json |
Scripts / jq / the bundled probe.py |
csv=p=0 |
Spreadsheet; p=0 strips the section name |
csv=s=x:p=0 |
Use x as separator (e.g. 1920x1080) |
flat |
streams.stream.0.width=1920 — easy to grep |
ini |
INI sections per stream |
xml |
XML (pair with -x pretty-print) |
Rule of thumb: json for automation, default=…nokey=1 for shell
one-liners, csv when you want to paste into a sheet.
Step 3 — Run it (recipes)
Full dump, JSON (the default starting point):
ffprobe -v error -show_format -show_streams -of json in.mp4
Duration in seconds (float):
ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 in.mp4
Resolution as 1920x1080:
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height -of csv=s=x:p=0 in.mp4
FPS as a fraction (30000/1001, 25/1, …):
ffprobe -v error -select_streams v:0 \
-show_entries stream=r_frame_rate \
-of default=nokey=1:noprint_wrappers=1 in.mp4
Parse the fraction yourself — it is not a float. See Gotchas.
Codec name (video / audio):
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \
-of default=nokey=1:noprint_wrappers=1 in.mp4
ffprobe -v error -select_streams a:0 -show_entries stream=codec_name \
-of default=nokey=1:noprint_wrappers=1 in.mp4
Bitrate (stream level first, then container fallback):
ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate \
-of default=nokey=1:noprint_wrappers=1 in.mp4
# fallback if "N/A":
ffprobe -v error -show_entries format=bit_rate \
-of default=nokey=1:noprint_wrappers=1 in.mp4
Audio channel layout / channel count / sample rate:
ffprobe -v error -select_streams a:0 \
-show_entries stream=channel_layout,channels,sample_rate \
-of default=noprint_wrappers=1 in.mp4
Color info (for HDR / BT.2020 / BT.709 classification):
ffprobe -v error -select_streams v:0 \
-show_entries stream=color_space,color_primaries,color_transfer,color_range,pix_fmt \
-of json in.mp4
HDR detection (full recipe):
ffprobe -v error -select_streams v:0 \
-show_entries stream=color_transfer,color_primaries,color_space:stream_side_data_list \
-of json in.mp4
Classify:
color_transfer=smpte2084+color_primaries=bt2020→ HDR10 (also checkmastering_display_metadata/content_light_levelinside_data_listfor HDR10).color_transfer=arib-std-b67→ HLG.side_data_type="DOVI configuration record"→ Dolby Vision.- Otherwise → SDR (usually
bt709).
Frame count (exact — SLOW, decodes whole stream):
ffprobe -v error -count_frames -select_streams v:0 \
-show_entries stream=nb_read_frames -of csv=p=0 in.mp4
Prefer the fast path first:
ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames \
-of default=nokey=1:noprint_wrappers=1 in.mp4
# "N/A" → fall back to -count_frames, or compute duration * fps.
Keyframe timestamps:
ffprobe -v error -select_streams v:0 -show_frames \
-show_entries frame=pkt_pts_time,pict_type \
-of csv=p=0 in.mp4 | awk -F, '$2=="I" {print $1}'
All packets (use intervals — raw -show_packets is huge):
ffprobe -v error -select_streams v:0 -read_intervals "10%+30" \
-show_packets -of json in.mp4
# reads 30 seconds starting 10% into the file
Chapters:
ffprobe -v error -show_chapters -of json in.mkv
Metadata tags (format + per-stream):
ffprobe -v error -show_entries format_tags:stream_tags -of json in.mp4
Available scripts
scripts/probe.py— stdlib-only wrapper. Subcommands:summary --input I— human-readable container / V / A summary.json --input I— full-show_format -show_streamsJSON to stdout.field --input I --query KEY— dotted path:format.duration,stream.v.width,stream.v.fps,stream.a.channels, …keyframes --input I— keyframe PTS times, one per line.hdr --input I— prints one ofSDR,HDR10,HLG,DolbyVision.compare --inputs A.mp4 B.mp4— side-by-side field diff.- Every subcommand supports
--verboseand--dry-run(prints the underlyingffprobecommand instead of executing).
Workflow
# Quick human summary:
uv run ${CLAUDE_SKILL_DIR}/scripts/probe.py summary --input in.mp4
# One field for a shell variable:
FPS=$(uv run ${CLAUDE_SKILL_DIR}/scripts/probe.py field --input in.mp4 --query stream.v.fps)
# HDR class:
uv run ${CLAUDE_SKILL_DIR}/scripts/probe.py hdr --input in.mp4
Reference docs
- Read
references/queries.mdfor the full-show_entriesfield table, stream-specifier syntax, output-format cheat sheet, an HDR detection flowchart, timebase math, and a 30+ one-liner library.
Gotchas
r_frame_rateis a FRACTION (30000/1001,24000/1001,25/1), not a float. Always parsenum/den.probe.pydoes this; raw shell consumers must too.avg_frame_rate≠r_frame_rateon VFR content.r_frame_rateis the lowest framerate that exactly describes every timestamp (effectively "base" rate);avg_frame_rate= total frames / duration. For CFR they match; for VFR useavg_frame_ratefor "how many FPS on average",r_frame_ratefor "what does each timestamp snap to".-count_framesdecodes the whole stream. Slow on long videos. Preferstream=nb_framesfirst; most MP4/MOV muxers write it. MKV usually does not.bit_rateat stream level is oftenN/Afor streams inside MKV/WebM and some fragmented MP4s. Fall back toformat.bit_rate, or computefile_size * 8 / duration.- Use
-v errorto silence stderr noise when piping (otherwise ffprobe prints banner + build info to stderr and callers assume it failed). - Stream specifier
v:0picks the first video stream — after-select_streams, the remaining stream's originalindexis still in its JSON, but counting positions in the filtered output is different. Always identify streams byindex, not by position. - Packet/frame timestamps are in timebase units. Seconds =
pts * time_base.num / time_base.den. ffprobe exposes*_timeconvenience fields (pkt_pts_time,best_effort_timestamp_time) already in seconds — prefer those. -read_intervals "10%+30"= start at 10% of duration, read 30 seconds."%+#20"= from start, 20 packets. Great for sampling huge files without dumping gigabytes of JSON.side_data_list(a.k.a.-show_entries stream_side_data_list) is where HDR10 mastering display, content light level, Dolby Vision config, and stereoscopic 3D layout live. Not in the main stream dict.- Raw files (headerless YUV/PCM) need explicit demuxer hints:
ffprobe -f rawvideo -video_size 1920x1080 -pix_fmt yuv420p -framerate 24 in.yuv. -show_entriessection syntax uses:between sections,,between fields:-show_entries format=duration:stream=codec_name,bit_rate.-of jsonis nested:{"streams":[{...}], "format":{...}}. Flatten withjqor-of flatif you need grep-friendly output.- "N/A" is a literal string, not null — parsers must coerce.
-hide_banneris implied when-v erroris set, but set both to be safe across ffmpeg versions.
Examples
Example 1: "What's the resolution and fps of this mp4?"
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate \
-of csv=p=0 in.mp4
# -> 1920,1080,30000/1001
Example 2: "Is this file HDR?"
uv run ${CLAUDE_SKILL_DIR}/scripts/probe.py hdr --input in.mp4
# -> HDR10
Example 3: "Get duration in seconds into a shell variable"
DUR=$(ffprobe -v error -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 in.mp4)
echo "$DUR" # 128.458667
Example 4: "Dump all keyframe timestamps to a file"
ffprobe -v error -select_streams v:0 -show_frames \
-show_entries frame=pkt_pts_time,pict_type \
-of csv=p=0 in.mp4 | awk -F, '$2=="I"{print $1}' > keyframes.txt
Example 5: "Compare two encodes"
uv run ${CLAUDE_SKILL_DIR}/scripts/probe.py compare --inputs orig.mp4 encoded.mp4
Troubleshooting
Error: Invalid data found when processing input
Cause: file is truncated, or container is wrong (e.g. .mp4 is actually a .ts).
Solution: try ffprobe -v error -show_format file (no -show_streams) to see
the detected format_name. If it's mpegts, treat as .ts. If ffprobe still
fails, the file is likely corrupt — try ffmpeg -err_detect ignore_err -i ….
bit_rate=N/A on every stream
Cause: muxer didn't write per-stream bitrate (common in MKV, WebM, fragmented MP4).
Solution: use format.bit_rate, or compute size_bytes * 8 / duration_seconds
from -show_entries format=size,duration.
r_frame_rate=0/0
Cause: codec/container has no concept of a frame rate (image, some audio-only
containers probed for a non-existent video stream).
Solution: guard against num==0 || den==0 in parsers; skip fps reporting.
nb_frames=N/A but I need an exact count
Cause: muxer doesn't store it. Solution: either compute
duration * avg_frame_rate (approximate, fine for CFR) or use -count_frames
(slow, exact).
Output is truncated / stops mid-JSON
Cause: you hit a pipe or buffer limit dumping -show_frames / -show_packets
on a long file.
Solution: always scope with -select_streams + -read_intervals; stream JSON
to a file with > rather than piping through an aggressive consumer.
Option not found for -show_entries
Cause: very old ffprobe (< 1.0). Solution: upgrade; on macOS
brew install ffmpeg.