FFprobe Tutorial: Inspect Videos, Extract Metadata & JSON

March 25, 2026 ยท RenderIO

You should always know what's in a video before you process it

You have a video. You need to know what's inside it before you do anything else. Resolution, codec, bitrate, frame rate, audio channels, duration. Maybe you're validating uploads before processing them. Maybe you're trying to figure out why a file won't play. Maybe you just need to confirm the codec before deciding whether to transcode or remux.

ffprobe is the tool for this. It ships with FFmpeg, reads any format FFmpeg supports, and outputs structured data you can pipe into scripts, parse in Python, or feed to an API. If you've been opening files in VLC and squinting at the codec info dialog, there's a better way.

What is ffprobe

ffprobe is a command-line utility bundled with FFmpeg. You don't install it separately. If you have FFmpeg, you have ffprobe.

It reads a media file (or URL, or stream) and prints information about the container format, each audio/video/subtitle stream, individual packets, and individual frames. It doesn't modify anything. It reads and reports.

The distinction matters: FFmpeg processes video, ffprobe inspects it. You use ffprobe to decide what FFmpeg command to run.

Install FFmpeg and you get ffprobe automatically:

# macOS
brew install ffmpeg

# Ubuntu/Debian
sudo apt install ffmpeg

# Windows (via scoop)
scoop install ffmpeg

# Verify it's there
ffprobe -version
# ffprobe version 8.1 Copyright (c) 2007-2026 the FFmpeg developers

As of March 2026, the latest stable FFmpeg release is 8.1. The ffprobe flags covered in this guide have been stable since FFmpeg 4.x, so version differences rarely matter here.

Basic usage

Point ffprobe at a file:

ffprobe input.mp4

You'll get a wall of text. FFmpeg's build configuration, library versions, then the actual file info at the bottom. The useful part looks like this:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input.mp4':
  Duration: 00:02:34.56, start: 0.000000, bitrate: 4523 kb/s
  Stream #0:0(und): Video: h264 (High), yuv420p, 1920x1080, 4391 kb/s, 30 fps
  Stream #0:1(und): Audio: aac (LC), 44100 Hz, stereo, fltp, 128 kb/s

That's your file in a nutshell: H.264 video at 1080p/30fps, AAC stereo audio, about 4.5 Mbps total.

Hide the banner

The build info is noise. Kill it:

ffprobe -hide_banner input.mp4

To suppress everything except actual errors:

ffprobe -hide_banner -v error input.mp4

The -v error flag sets the log level. Options range from quiet (nothing), panic, fatal, error, warning, info (default), to verbose and debug. For scripting, -v error is the sweet spot. You still see real problems but skip the noise.

Inspecting streams

The -show_streams flag dumps detailed info about every stream in the file:

ffprobe -hide_banner -v error -show_streams input.mp4

This prints codec name, profile, resolution, pixel format, frame rate, bitrate, sample rate, channel layout, and dozens of other fields wrapped in [STREAM]...[/STREAM] tags.

Select specific streams

Most files have at least two streams (video + audio). Use -select_streams to inspect just one:

# Video stream only
ffprobe -hide_banner -v error -select_streams v:0 -show_streams input.mp4

# Audio stream only
ffprobe -hide_banner -v error -select_streams a:0 -show_streams input.mp4

# First subtitle track
ffprobe -hide_banner -v error -select_streams s:0 -show_streams input.mp4

The specifier v:0 means "first video stream." a:0 is first audio. s:0 is first subtitle track. If a file has multiple audio tracks (common in MKV files with multiple languages), a:1 gives you the second one.

Container format info

To see the container-level metadata (duration, bitrate, format name, tags):

ffprobe -hide_banner -v error -show_format input.mp4

Output:

[FORMAT]
filename=input.mp4
nb_streams=2
format_name=mov,mp4,m4a,3gp,3g2,mj2
duration=154.560000
size=87345920
bit_rate=4523156
[/FORMAT]

If you've ever needed to figure out why a file is larger than expected, the bit_rate and duration fields here are your starting point. From there, the video compression guide covers how to bring that bitrate down without visible quality loss.

Output formats: JSON, CSV, XML

The default output format uses [SECTION]...[/SECTION] wrappers. Fine for reading, annoying for parsing. ffprobe supports several machine-readable formats.

JSON output

This is the one you'll use most:

ffprobe -hide_banner -v error -print_format json -show_streams -show_format input.mp4
{
    "streams": [
        {
            "index": 0,
            "codec_name": "h264",
            "codec_type": "video",
            "width": 1920,
            "height": 1080,
            "r_frame_rate": "30/1",
            "bit_rate": "4391000",
            "pix_fmt": "yuv420p"
        },
        {
            "index": 1,
            "codec_name": "aac",
            "codec_type": "audio",
            "sample_rate": "44100",
            "channels": 2,
            "bit_rate": "128000"
        }
    ],
    "format": {
        "filename": "input.mp4",
        "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
        "duration": "154.560000",
        "bit_rate": "4523156"
    }
}

Now you can pipe this into jq, parse it in Python, feed it to Node.js, whatever. The -print_format json flag (or -of json, same thing) is what makes ffprobe actually useful in automation. You can also use -of json=compact=1 for a denser output format.

CSV output

ffprobe -hide_banner -v error -print_format csv -show_streams input.mp4

Each stream becomes one comma-separated line. Useful for quick spreadsheet dumps or awk processing.

XML output

ffprobe -hide_banner -v error -print_format xml -show_streams input.mp4

If you're feeding data into something that expects XML. Some enterprise media systems still do.

Flat output

ffprobe -hide_banner -v error -print_format flat -show_streams input.mp4

Produces lines like streams.stream.0.codec_name="h264". Each field is a dot-separated path. Grep-friendly, and particularly useful if you want to diff two files:

diff <(ffprobe -v error -of flat -show_streams source.mp4) \
     <(ffprobe -v error -of flat -show_streams output.mp4)

Extracting specific fields

You don't always need everything. The -show_entries flag lets you pull exactly the fields you want:

# Just resolution
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=width,height \
  -of csv=p=0 input.mp4

Output: 1920,1080

The -of csv=p=0 part means "output as CSV, don't print section headers." Clean, single-line output you can capture in a variable.

More examples:

# Duration in seconds
ffprobe -hide_banner -v error \
  -show_entries format=duration \
  -of csv=p=0 input.mp4
# Output: 154.560000

# Codec name and bitrate for video stream
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=codec_name,bit_rate \
  -of csv=p=0 input.mp4
# Output: h264,4391000

# Frame rate
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=r_frame_rate \
  -of csv=p=0 input.mp4
# Output: 30/1

# Pixel format
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=pix_fmt \
  -of csv=p=0 input.mp4
# Output: yuv420p

# Total number of frames (fast, reads from header)
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=nb_frames \
  -of csv=p=0 input.mp4
# Output: 4637

# Audio channel layout
ffprobe -hide_banner -v error -select_streams a:0 \
  -show_entries stream=channel_layout,channels \
  -of csv=p=0 input.mp4
# Output: stereo,2

This is how you build reliable scripts. Grab one field, check it, act on it. The FFmpeg cheat sheet has 50 commands organized by task, and knowing how to inspect files with ffprobe first makes choosing the right command much easier.

Packet and frame analysis

For debugging encoding issues, you can go deeper than stream-level info.

Packet info

ffprobe -hide_banner -v error -select_streams v:0 \
  -show_packets -of json input.mp4 | head -50

Each packet includes pts, dts, duration, size, and flags. The flags field tells you if a packet contains a keyframe (K) or not. This matters when you're trimming video โ€” cuts on non-keyframes with -c copy result in frozen or garbled frames at the start of the clip.

Frame info

ffprobe -hide_banner -v error -select_streams v:0 \
  -show_frames -of json input.mp4 | head -80

Frame-level data includes pict_type (I, P, or B frame), key_frame flag, width, height, and timestamps. If you need to understand keyframe distribution before extracting frames, this is how you find them.

Count keyframes

A quick way to count I-frames in a file:

ffprobe -hide_banner -v error -select_streams v:0 \
  -show_frames -show_entries frame=pict_type \
  -of csv=p=0 input.mp4 | grep -c I

If you're seeing too few keyframes (say, one every 10 seconds), that explains why seeking feels sluggish in your player. Too many keyframes and the file is larger than it needs to be. For a typical 30fps video, one keyframe every 2-4 seconds (every 60-120 frames) is a reasonable interval.

Practical ffprobe examples

Here are 10 commands that cover the most common real-world scenarios.

1. Quick file summary as JSON

ffprobe -hide_banner -v error -print_format json \
  -show_format -show_streams input.mp4

The everything-dump. Pipe it to a file, search later.

2. Check if a file has audio

ffprobe -hide_banner -v error -select_streams a \
  -show_entries stream=codec_name -of csv=p=0 input.mp4

If this returns nothing, the file has no audio track. Useful for validating uploads before sending them to an FFmpeg API for processing.

3. Get video dimensions for resize decisions

WIDTH=$(ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=width -of csv=p=0 input.mp4)
HEIGHT=$(ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=height -of csv=p=0 input.mp4)
echo "${WIDTH}x${HEIGHT}"

4. Detect codec before transcoding

ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=codec_name -of csv=p=0 input.mp4

If this returns h264 and you need h265, you need to transcode. If the codec already matches your target, just remux with -c copy and save the CPU time.

5. Verify metadata was stripped

After running a metadata strip command, confirm it worked:

ffprobe -hide_banner -v error -show_format -show_entries format_tags input.mp4

If the TAG: lines are gone (or only contain what you expect), the metadata stripping was successful. This step is especially relevant for removing AI metadata since tools like Runway, Kling, and HeyGen embed generation info in format tags that you'll want to verify are actually gone.

6. Check bitrate for compression decisions

BITRATE=$(ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=bit_rate -of csv=p=0 input.mp4)
echo "Video bitrate: $((BITRATE / 1000)) kbps"

If you're seeing 15 Mbps on a 1080p talking-head video, there's room to compress. If it's already at 2 Mbps, pushing further will cost you quality.

7. Get the rotation tag (phone recordings)

Phone-recorded videos often have a rotation metadata tag instead of actually rotated pixels:

ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream_side_data=rotation \
  -of csv=p=0 input.mp4

If this returns 90 or 270, the video is portrait but the pixels are landscape. FFmpeg handles this automatically during transcoding, but if you're using -c copy, the rotation tag carries over as-is.

8. Check if a file is VFR (variable frame rate)

Compare the real and average frame rates:

ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=r_frame_rate,avg_frame_rate \
  -of csv=p=0 input.mp4

If r_frame_rate and avg_frame_rate differ (for example, 30/1 vs 24000/1001), you're dealing with VFR content. Screen recordings and phone footage are common culprits. This matters because some editors and encoders don't handle VFR well.

9. Inspect a remote URL without downloading

ffprobe -hide_banner -v error -print_format json \
  -show_format -show_streams \
  "https://example.com/video.mp4"

ffprobe downloads just enough of the file to read the headers and metadata. For MP4 files with faststart (moov atom at the beginning), this means only the first few kilobytes. Useful for checking files in S3 or cloud storage before deciding whether to process them.

10. Compare source and output after processing

echo "=== SOURCE ===" && \
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=codec_name,width,height,bit_rate \
  -show_entries format=size,duration \
  -of flat source.mp4 && \
echo "=== OUTPUT ===" && \
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_entries stream=codec_name,width,height,bit_rate \
  -show_entries format=size,duration \
  -of flat output.mp4

This is the "before and after" check. Run it after any FFmpeg operation to confirm you got what you expected.

ffprobe in automation scripts

This is where ffprobe earns its keep: deciding what to do before you do it.

Pre-upload validation (bash)

#!/bin/bash
validate_video() {
  local file="$1"

  CODEC=$(ffprobe -hide_banner -v error -select_streams v:0 \
    -show_entries stream=codec_name -of csv=p=0 "$file")
  WIDTH=$(ffprobe -hide_banner -v error -select_streams v:0 \
    -show_entries stream=width -of csv=p=0 "$file")
  DURATION=$(ffprobe -hide_banner -v error \
    -show_entries format=duration -of csv=p=0 "$file")

  # Reject if not H.264
  if [ "$CODEC" != "h264" ]; then
    echo "REJECT: codec is $CODEC, expected h264"
    return 1
  fi

  # Reject if width under 720
  if [ "$WIDTH" -lt 720 ]; then
    echo "REJECT: width is ${WIDTH}px, minimum is 720"
    return 1
  fi

  # Reject if longer than 10 minutes
  if (( $(echo "$DURATION > 600" | bc -l) )); then
    echo "REJECT: duration is ${DURATION}s, max is 600"
    return 1
  fi

  echo "OK: ${CODEC} ${WIDTH}px ${DURATION}s"
  return 0
}

validate_video "$1"

Batch inspection (Python)

import subprocess
import json

def probe(filepath):
    result = subprocess.run(
        ["ffprobe", "-hide_banner", "-v", "error",
         "-print_format", "json",
         "-show_format", "-show_streams", filepath],
        capture_output=True, text=True
    )
    return json.loads(result.stdout)

info = probe("input.mp4")
video = next(s for s in info["streams"] if s["codec_type"] == "video")
print(f"Resolution: {video['width']}x{video['height']}")
print(f"Codec: {video['codec_name']}")
print(f"Duration: {info['format']['duration']}s")

This pattern works well as the first step in a processing pipeline. Probe the file, decide what needs to happen (transcode, compress, reject), then send the appropriate FFmpeg command to RenderIO's API for cloud processing.

Node.js probe + API pipeline

const { execSync } = require("child_process");

function probe(filepath) {
  const output = execSync(
    `ffprobe -hide_banner -v error -print_format json -show_format -show_streams "${filepath}"`,
    { encoding: "utf-8" }
  );
  return JSON.parse(output);
}

async function processVideo(filepath) {
  const info = probe(filepath);
  const video = info.streams.find((s) => s.codec_type === "video");

  if (parseInt(video.width) > 1920) {
    // Needs downscaling โ€” send to RenderIO
    const res = await fetch(
      "https://renderio.dev/api/v1/run-ffmpeg-command",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-API-KEY": process.env.RENDERIO_KEY,
        },
        body: JSON.stringify({
          ffmpeg_command:
            "-i {{in_video}} -vf scale=1920:-2 -c:v libx264 -crf 23 {{out_video}}",
          input_files: { in_video: uploadUrl },
          output_files: { out_video: "resized.mp4" },
        }),
      }
    );
    return res.json();
  }

  console.log("Video is already 1920px or smaller, no resize needed");
}

Probe locally, process in the cloud. You skip uploading files that don't need processing, and the heavy encoding runs on someone else's CPUs. The complete API guide walks through authentication, file uploads, and webhook callbacks.

n8n workflow integration

If you use n8n for automation, you can skip running ffprobe locally entirely. RenderIO's API response includes file metadata (duration, codec, resolution, file size) in the response payload. The n8n video processing guide covers how to set up these workflows. But if you need probe data before sending a job, a simple Execute Command node with the ffprobe one-liners above works just as well.

Common gotchas

r_frame_rate vs avg_frame_rate: The r_frame_rate field is the "real" frame rate based on the stream's time base. avg_frame_rate divides total frames by duration. For constant frame rate video they match. For variable frame rate (screen recordings, phone footage), they can differ significantly. Use avg_frame_rate for VFR content.

Duration in format vs stream: The format section duration covers the entire file. Individual stream durations might differ slightly (audio tracks are often a fraction of a second longer due to codec padding). Use format duration for display purposes, stream duration for precise calculations.

Bitrate might be "N/A": Some containers (like raw H.264 streams or certain MKV files) don't store bitrate in the header. ffprobe reports N/A. You can calculate it yourself: file_size_bits / duration_seconds. In bash: echo "scale=0; $(stat -c%s file.mp4) * 8 / $(ffprobe -v error -show_entries format=duration -of csv=p=0 file.mp4 | cut -d. -f1)" | bc.

-show_frames is slow on large files: This decodes the entire file to report frame-level data. On a 2-hour movie, expect it to take minutes. Use -read_intervals to limit the scan to a specific time range:

# First 10 seconds only
ffprobe -hide_banner -v error -select_streams v:0 \
  -show_frames -read_intervals "%+10" \
  -of json input.mp4

nb_frames might say "N/A" too: Some containers don't store the frame count in the header. Use -count_frames with -show_entries stream=nb_read_frames to get an exact count, but be aware this decodes the whole file, same performance caveat as -show_frames.

ffprobe vs ffmpeg -i vs MediaInfo

People sometimes use ffmpeg -i input.mp4 as a quick inspection tool. It works, but it's a hack. FFmpeg prints file info to stderr as part of its startup, then exits with an error because you didn't specify an output. ffprobe is the purpose-built tool for this and gives you structured output formats.

MediaInfo is another option. It has a GUI and reads some metadata that ffprobe doesn't (like specific HDR parameters or Dolby Vision profile details). But for command-line scripting and anything you're automating, ffprobe is simpler and already installed if you have FFmpeg.

Frequently asked questions

How do I get just the video duration with ffprobe?

ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4

This returns the duration in seconds as a decimal (e.g., 154.560000). For a human-readable format, use format=duration with the default output format and you'll see duration=154.560000.

Can ffprobe read HLS streams or RTMP URLs?

Yes. Anything FFmpeg can read, ffprobe can inspect. Pass the URL directly:

ffprobe -hide_banner -v error -show_streams "https://example.com/stream/playlist.m3u8"

For live streams, ffprobe will read the manifest and report the stream parameters without downloading the entire broadcast.

What's the difference between -print_format json and -of json?

They're the same flag. -of is a shorter alias for -print_format. Both work identically.

How do I check if a video has subtitles?

ffprobe -hide_banner -v error -select_streams s \
  -show_entries stream=codec_name,codec_type \
  -of csv=p=0 input.mkv

If this returns nothing, no subtitle streams exist. Otherwise you'll see entries like subrip,subtitle or ass,subtitle.

Can I use ffprobe with multiple files at once?

Not directly. ffprobe takes one input at a time. For batch work, loop in bash:

for f in *.mp4; do
  echo "$f: $(ffprobe -v error -select_streams v:0 \
    -show_entries stream=width,height,codec_name -of csv=p=0 "$f")"
done

Does ffprobe modify the file at all?

No. ffprobe is strictly read-only. It will never write to, modify, or lock your files.

Start inspecting, then process

ffprobe should be the first command you run before any FFmpeg job. Know what's in the file before you decide what to do with it. The JSON output makes it easy to script, and pairing it with a cloud FFmpeg service means you can build pipelines where inspection runs locally (fast, free) and encoding runs in the cloud.

If you want to skip managing FFmpeg infrastructure yourself, grab an API key and run commands over HTTP instead. The complete FFmpeg API guide covers authentication, uploads, and webhooks. For quick reference, the FFmpeg cheat sheet has 50 commands organized by task, and the commands reference pairs each one with the matching API call.