How to get video duration with ffprobe

Read a video's duration with ffprobe when three fields disagree. Measured across 200 probe jobs, showing which field to trust and where the gap hits 493 ms.

Share

Short version

Use this:

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

That prints one number and nothing else. It is the duration a player will show.

It is also not the only duration in the file, and the others do not agree with it. We went back through 200 completed ffprobe jobs already recorded in Rendobar’s job history and pulled every one that reported both a container duration and per-stream durations. Eleven distinct media files came out of that. Four disagreed. All four were MP4 files carrying both a video and an audio stream. The one video-only MP4 agreed exactly.

The three durations

format.duration is the container’s number. It is what -show_format prints, what players show, and what the one-liner above returns.

Each stream also carries its own duration, which is what -show_streams prints per stream. A file with video and audio has at least two of these.

Then there is the number nobody reads by default, the count you get by decoding frames instead of trusting a header. That needs -count_frames, and it costs real time because ffprobe walks the packets rather than reading a box near the front of the file.

Three numbers, three different questions. Most duration bugs are asking one and reading another.

What the files actually said

Every gap in the sample points the same way.

FileContainerVideo streamAudio streamGap
sample.mp45.013 s5.000 s5.013 s13 ms
sea.mp43.008 s3.000 s3.008 s8 ms
clip.mp43.008 s3.000 s3.008 s8 ms
file_example_MP4_480_1_5MG.mp430.526667 s30.033333 s30.526667 s493 ms
a video-only MP410.000 s10.000 snone0 ms

In all four disagreements, the container duration equals the audio duration exactly, to the last decimal place. Not approximately. The same digits.

That is the mechanism and it is not an ffprobe quirk. The container reports the length of its longest stream, and in an MP4 with AAC the audio is longer than the video almost by construction. AAC encodes in fixed 1,024-sample frames, so the encoder pads the final frame out to a full one and adds priming samples at the start. The video stops on a frame boundary at 24 or 30 fps. The two rarely land together.

Why 493 ms is the number to worry about

Eight milliseconds is under a frame at 24 fps and nothing will notice. 493 ms is twelve frames.

Trim to format.duration on that 30.5 second file and you keep 493 ms of audio playing over nothing. Concatenate two such clips and every join carries the same half second of black or frozen frame, and the drift compounds down the timeline. Build a subtitle track against the container duration, burn it against the video, and the last cue lands past the last frame.

The rule that falls out of this: read format.duration when you are telling a human how long something is, and read the video stream duration when you are cutting. They answer different questions, and in our sample they never agreed on a file with sound.

The commands worth memorising

Across the archive, 200 completed probe jobs ran 25 distinct ffprobe command shapes. These four cover most of what people actually need.

Terminal window
# Duration only, machine readable. Nothing but the number.
ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4
# Video stream duration, the one that matters when cutting.
ffprobe -v error -select_streams v:0 -show_entries stream=duration -of csv=p=0 input.mp4
# Everything as JSON, for looking rather than parsing one field.
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4
# Frame-exact. Decodes packets, so it is the slow one.
ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of csv=p=0 input.mp4

Probe execution across those 200 jobs had a median of 171 ms, a 10th-to-90th spread of 94 ms to 411 ms, and a slowest run of 4,367 ms. That 46x spread is not ffprobe being unpredictable. It tracks how much of the file had to come over the network before the header was readable, which is why a probe against an MP4 with a front-loaded moov atom is fast and one against a file that buried it at the end is not.

Billing followed a floor rather than the work. Every one of the 169 probe jobs run since 2026-07-20 billed exactly $0.0010, with no variance at all, whether the input was a 40 KB JPEG or a six-minute MP3.

Reading it from a script

The CSV form exists so you do not have to parse JSON for one number.

Terminal window
DURATION=$(ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4)
echo "$DURATION" # 5.013000

-v error suppresses the banner, which otherwise lands on stderr and confuses anything reading both streams. -of csv=p=0 drops the section header, so you get 5.013000 rather than format,5.013000.

Two failure modes are worth a guard. Some containers report N/A instead of a number, and streamed input often has no duration at all until enough of it has been read. Both come back as the literal string N/A rather than an empty result, so a naive float parse throws instead of returning zero.

Running it without installing ffprobe

Rendobar’s FFmpeg API takes the same command over HTTP and returns both the raw ffprobe report and a normalised summary, so the container-versus-stream distinction is already resolved in the response.

job.ts
import { createClient } from "@rendobar/sdk";
const rb = createClient({ apiKey: process.env.RENDOBAR_API_KEY });
const job = await rb.jobs.run({
type: "ffprobe",
params: {
command:
"ffprobe -v error -show_entries format=duration -of csv=p=0 https://cdn.rendobar.com/assets/examples/sample.mp4",
},
});
// The normalised summary resolves the container-versus-stream question for you.
console.log(job.output.data.summary.durationSec); // 5.013
console.log(job.output.data.streams[0].duration); // "5.000000"

Install with npm i @rendobar/sdk. jobs.run() submits and waits, so it returns the finished job in one call.

terminal
curl -X POST https://api.rendobar.com/jobs -H "Authorization: Bearer $RENDOBAR_API_KEY" -H "Content-Type: application/json" -d '{
"type": "ffprobe",
"params": { "command": "ffprobe -v error -show_entries format=duration -of csv=p=0 https://cdn.rendobar.com/assets/examples/sample.mp4" }
}'

Returns immediately with a job id. Poll GET /jobs/{id} or register a webhook rather than blocking on the request.

The command runs as written. Defaults for JSON output, format, streams and chapters fill in only for flags you did not pass, so a hand-typed command behaves the way it does locally.

Where this stops

Four disagreeing files is a small sample, and all four are MP4 with AAC audio. The mechanism generalises, because a container reports its longest stream and AAC padding makes audio the longest stream, but we have not measured MKV, WebM, MOV or fragmented MP4, and we have not tested Opus or Vorbis, where the padding behaviour differs.

Every number here comes from probe jobs already in Rendobar’s job history rather than from a fresh benchmark run, so the timings carry real production variance. Nothing in the duration comparison depends on timing. Durations are properties of the file and repeat exactly.

The 493 ms figure is one file. Treat it as evidence that the gap gets large enough to see, not as a typical value.

For the wider set of ffprobe fields that mislead, see ffprobe metadata gotchas. For the full command set with measured cost, see common ffprobe commands compared.

Frequently asked questions

What is the ffprobe command to get video duration?

Run ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4. It prints the duration in seconds and nothing else, which is what you want inside a script. Drop the -of flag and you get a labelled section instead.

Why does ffprobe show two different durations?

A media file carries one duration per stream plus one for the container, and they are separate numbers. The container reports the length of its longest stream. In an MP4 with audio that is almost always the audio stream, because AAC encoders add priming samples and pad the final frame out to a full one.

Should I use format.duration or stream duration?

Use format.duration for playback length, which is what a player shows. Use the video stream duration when you are cutting, concatenating or overlaying, because that is when the last video frame actually lands. In our sample those two numbers differed by up to 493 ms.

How do I get the duration of a video with no audio?

The same command works. On the one video-only MP4 in our sample, format.duration and the video stream duration matched exactly at 10 seconds, because with a single stream there is nothing for the container to round up to.

Is ffprobe duration accurate?

It is accurate to what the file declares, which is not the same as counting frames. For a frame-exact number use -count_frames -select_streams v:0 -show_entries stream=nb_read_frames, and expect it to be slower because it decodes packets instead of reading a header.

Sources

Tags #ffprobe#ffmpeg#video-metadata#media
All posts
Share
  1. Opus vs AAC vs MP3 vs FLAC Engineering blog
  2. AV1 vs H.264 VMAF compared Engineering blog
  3. AV1 vs VP9 vs HEVC vs H.264 Engineering blog