Validate video uploads with ffprobe
Probe an uploaded file to confirm it is real media before you pay to encode it. Measured on 213 jobs, including 3 named .mp4 that were not video at all.
Short version
Run ffprobe on the file before you do anything else with it. If it does not parse, it is not media, and nothing downstream will make it one.
We pulled the full probe history out of Rendobar’s job archive: 213 ffprobe jobs, 200 completed, 13 did not. Reading the 13 failures is the whole argument for this post.
- 9 could not fetch the input at all. Dead URL, wrong host, expired link.
- 3 fetched successfully and then failed to parse. Every one of them was named
.mp4. The bytes were not video. - 1 was dispatched and never started, which is an infrastructure failure rather than a file problem.
The three in the middle are the interesting group. The filename said MP4. The upload succeeded. Nothing downstream would have caught it before an encoder did.
The extension is a claim, not a fact
Both signals people reach for are supplied by the caller.
The file extension is part of a name the uploader chose. Renaming report.pdf to report.mp4 takes a keystroke, and users do it by accident constantly, usually while trying to “convert” something.
The Content-Type header is set by the client. A browser guesses it from the extension. A script sets it to whatever the author typed. Neither has looked at the bytes.
What actually identifies a file is its header. ffprobe reads that header, matches it against every demuxer it has, and tells you what it found along with a probe_score for how confident the match is. A well-formed MP4 comes back with "probe_score": 100. Something that is not media comes back as an error, not a low score.
The four checks
Everything worth validating is a field in one probe.
ffprobe -v error -print_format json -show_format -show_streams input.mp41. Does it parse. If the command exits non-zero, stop. There is no partial credit here. Our three bad uploads all failed at this step, before any field mattered.
2. Does it have the stream you need. Walk streams and look for a codec_type of video or audio. Do not infer this from the container, because an MP4 can be audio-only and an MP3 with cover art reports a video stream that is a single still image. The tell for cover art is disposition.attached_pic set to 1.
3. Is the duration present and plausible. format.duration can be missing entirely on a stream, and it can be nonsense on a still image, where the image2 demuxer invents 0.04 seconds. A missing duration is not automatically a rejection, but it does mean you cannot estimate cost before running the job.
4. Is it within what you will pay to encode. Resolution, frame rate and bitrate are all in the stream entry. This is where you decide that an 8K 120 fps input is not something your free tier is going to transcode.
What it costs to check
The economics are the reason to put a probe in front of everything rather than only in front of suspicious things.
A probe on Rendobar bills a flat $0.0010, and that number has not moved across the 169 consecutive probe jobs run since 2026-07-20. It does not scale with file size. A 40 KB JPEG and a six-minute MP3 bill the same.
An FFmpeg job in the same archive has a median of $0.0025 across its last 425 runs, with a 90th percentile of $0.0042. So the probe is roughly 40% of a median encode, and it eliminates the case where you pay for an encode that was always going to fail.
Speed is not the obstacle either. Across 200 completed probes the median was 171 ms, the 10th to 90th percentile band was 94 ms to 411 ms, and the slowest single run was 4,367 ms. A probe in front of a job adds a fifth of a second to the common case.
Validating without installing FFmpeg
The probe runs over HTTP with the command as a string, which means the validation step does not need FFmpeg in your application container.
import { createClient } from "@rendobar/sdk";
const rb = createClient({ apiKey: process.env.RENDOBAR_API_KEY });
export async function isUsableVideo(url: string) {const job = await rb.jobs.run( { type: "ffprobe", params: { command: url } }, { throwOnFailure: false },);
// 1. Did it parse at all. A failed probe means it is not media.if (job.status !== "complete") return { ok: false, reason: job.error?.message };
const { summary } = job.output.data;
// 2. Does it carry the stream we need. 3. Is the duration plausible.if (!summary.video) return { ok: false, reason: "no video stream" };if (!summary.durationSec) return { ok: false, reason: "no duration" };
// 4. Is it inside what we are willing to encode.if (summary.video.width > 3840) return { ok: false, reason: "too large" };
return { ok: true, summary };}Install 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 -print_format json -show_format -show_streams 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 response carries the raw ffprobe report plus a normalised summary with the container, duration, resolution, fps, rotation, HDR signalling and audio layout already resolved. For validation the summary is usually enough, because the four checks above map onto four of its fields.
A job that fails carries a message describing which stage failed. In our 13 failures the messages split cleanly into “could not fetch input for probing” for the 9 unreachable URLs and “ffprobe could not read the input” for the 3 files that were not media. Those two strings are different problems with different fixes, and telling a user “we could not download your file” when the truth is “your file is not a video” wastes everyone’s time.
What a probe does not tell you
It reads the header. It does not decode the whole file.
So a probe will happily accept a file whose header is fine and whose frames are corrupt halfway through. It will accept a video that is entirely black. It will accept an audio track that is pure silence. None of those are header problems and none of them show up in -show_format.
If you need more than a header check, -count_frames decodes packets and gives you a real frame count, which catches truncation. It is genuinely slower, because it walks the file rather than reading a box near the front of it. Reserve it for the cases where the cost of accepting a broken file is higher than the cost of the check.
There is also a security boundary this does not cross. ffprobe parsing a file means a demuxer touched attacker-controlled bytes. Probing is a validation step, not a sandbox, and running it somewhere isolated from your application is a separate decision from running it at all.
Where this stops
Thirteen failures is a small sample and it comes from our own testing traffic rather than from a production upload funnel with real users attached, so the 9-to-3-to-1 split describes what we happened to break rather than what a public form would see. The direction is what matters: most failures never reached the file, and the ones that did were misnamed.
Every figure here comes from probe jobs already recorded in the job history rather than a fresh benchmark, so the timings carry real production variance. The costs do not vary at all, which is a property of the flat rate rather than a property of the measurement.
Nothing here covers upload-time limits, virus scanning, or the storage side of accepting files. It covers one question, which is whether the thing you just received is the kind of media you think it is.
For the fields that mislead once a file does parse, see ffprobe metadata gotchas. For the full command set with measured cost, see common ffprobe commands compared. For the duration field specifically, which disagrees with itself by up to 493 ms, see how to get video duration with ffprobe.
Frequently asked questions
How do I check if an uploaded file is really a video?
Run ffprobe on it and see whether it parses. If ffprobe cannot read the header it is not media, regardless of what the filename or the Content-Type header claims. In our archive 3 files named .mp4 failed exactly this way.
Can I trust the file extension or MIME type of an upload?
No. Both are supplied by whoever uploaded the file. An attacker sets them freely and an ordinary user gets them wrong by accident, usually by renaming a file instead of converting it.
What should I validate before processing a video?
Four things. That ffprobe can parse it at all, that it contains the stream type you need, that the duration is present and plausible, and that the resolution and bitrate are inside what you are willing to spend on. Each is one field in the probe output.
Does validating an upload with ffprobe cost anything?
On Rendobar a probe bills a flat $0.0010 no matter the input. The median FFmpeg job in the same archive bills $0.0025, so a probe costs about 40% of a median encode and saves you the whole encode when the file is bad.
How do I detect a video with no audio track?
Look at the stream list and check whether any entry has codec_type of audio. Do not infer it from the container. A file can carry an audio track that is silent, and a file can carry a video stream that is only cover art.
