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.
Use this to get a video’s duration with ffprobe:
ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4# 5.013000It 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:
ffprobe -v error -show_entries format=duration:stream=codec_type,duration -of json input.mp4The 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.
| File | Container | Video stream | Audio stream | Gap |
|---|---|---|---|---|
sample.mp4 | 5.013 s | 5.000 s | 5.013 s | 13 ms |
sea.mp4 | 3.008 s | 3.000 s | 3.008 s | 8 ms |
clip.mp4 | 3.008 s | 3.000 s | 3.008 s | 8 ms |
file_example_MP4_480_1_5MG.mp4 | 30.526667 s | 30.033333 s | 30.526667 s | 493 ms |
| a video-only MP4 | 10.000 s | 10.000 s | none | 0 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
# 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# 120The 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.
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.
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 showsconsole.log(video.duration); // "5.000000", where the last frame landsInstall with npm i @rendobar/sdk. jobs.run() submits and waits, so it returns the finished job in one call.
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.
