Get video duration with ffprobe, container vs stream

Read a video duration with ffprobe when the container and streams disagree. On MP4s with audio the container matched the audio, up to 493 ms past the video.

Share

Use this to get a video’s duration with ffprobe:

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

It prints one number, the duration a player will show. It is not the only duration in the file, and on every MP4 with sound that I checked, the video stream’s own duration was shorter. Read the container number when you tell a person how long something is, and the video stream’s number when you cut.

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, and this command prints all of them at once:

Terminal window
ffprobe -v error -show_entries format=duration:stream=codec_type,duration -of json input.mp4

The third number is the one you get by decoding frames instead of trusting a header. That needs -count_frames, and it takes time in proportion to the file, because ffprobe walks every packet instead of reading one box near the front.

Three numbers, three different questions. Most duration bugs ask one and read another.

What the files said

I went back through the completed ffprobe jobs in Rendobar’s job history and took every distinct file that reported both a container duration and per-stream durations. The files with a single stream agreed with themselves: a video-only MP4, two MP3s, a WAV and two JPEGs. The four MP4s carrying both video and audio all disagreed, and every gap 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 to the last decimal place. The same digits, not a close match.

The cause is how the container counts, not ffprobe. 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 a 493 ms gap matters

Eight milliseconds is under a frame at 24 fps and nothing will notice. 493 ms is about 15 frames at that file’s 30 fps.

That 30.5 second file already carries 493 ms of audio past its last frame. Cut at format.duration and you keep all of it, playing over a frozen or black picture. Concatenate two such clips and every join carries the same half second, and the drift compounds down the timeline. Time a subtitle track against the container duration, burn it against the video, and the last cue lands past the last frame.

So read format.duration when you are telling a human how long something is, and read the video stream duration when you are cutting.

The duration commands

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
# 5.000000
# 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
# 120

The rest of the everyday set, and what each one prints, is in common ffprobe commands and what each one prints.

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 name, so you get 5.013000 and not format,5.013000.

Guard against one failure mode. Some containers report no duration, and streamed input often has none until enough of it has been read. Both come back as the literal string N/A, not an empty result, so a naive float parse throws instead of returning zero.

Running it without installing ffprobe

Rendobar’s FFmpeg API runs the same command over HTTP. With a JSON writer the job returns the parsed report, so both numbers are one property access away. Pick the video stream by codec_type, not by position, because nothing guarantees it comes first.

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:stream=codec_type,duration -of json https://cdn.rendobar.com/assets/examples/sample.mp4",
},
});
const { format, streams } = job.output.data;
const video = streams.find((s) => s.codec_type === "video");
console.log(format.duration); // "5.013000", what a player shows
console.log(video.duration); // "5.000000", where the last frame lands

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:stream=codec_type,duration -of json 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 job’s normalised summary.durationSec is the container number, 5.013 here, so it answers the playback question and not the cutting one.

Every file here is MP4 with AAC audio, and the 493 ms gap is one file. The mechanism generalises, because a container reports its longest stream and AAC padding makes audio the longest, but I have not measured MKV, WebM, MOV or fragmented MP4, or Opus and Vorbis audio, where the padding behaves differently. Durations are properties of the file and repeat exactly, so none of this depends on timing.

If I were writing a trim or concat step today, I would read the video stream’s duration and never the container’s. It costs one extra -select_streams, and on every file with sound I checked, the container number ran past the last frame.

Frequently asked questions

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 divide by the frame rate. It decodes packets, so it takes longer on a long file.

How do I get the duration as hours, minutes and seconds with ffprobe?

Add -sexagesimal. ffprobe -v error -sexagesimal -show_entries format=duration -of csv=p=0 input.mp4 prints 0:00:05.013000 for the 5-second sample clip.

Sources

Tags #ffprobe#ffmpeg#video-metadata#media
All posts
Share
  1. How we benchmark FFmpeg Engineering blog
  2. Custom fonts in a video API fail silently Engineering blog
  3. Opus vs AAC vs MP3, requested vs delivered bitrate Engineering blog