Get video duration and codec in PHP without ffprobe installed

You can get video duration, dimensions and codec in PHP three ways, and the only thing that decides which one you use is whether ffprobe is installed where your code actually runs.
- Shell out to
ffprobe -v quiet -print_format json -show_format -show_streamsandjson_decode()the output, when you control the box andexecisn't indisable_functions. - Install PHP-FFMpeg with
composer require php-ffmpeg/php-ffmpegand read metadata off itsFFProbeobject, which is a nicer API over that same binary. - POST the file's URL to a hosted ffprobe endpoint and parse the identical
streamsarray, when there is no binary and no shell, which describes most cPanel hosting and every slim PHP image without an FFmpeg layer. - Whichever route you pick, take duration from
format.durationwith the video stream as the tiebreaker, and check the video stream'sside_data_listfor rotation before you believewidthandheight.
That last line is the part page one skips, and it's the reason a correct-looking PHP validator rejects correct files. More on it below.
Shelling out to ffprobe from PHP
The canonical invocation prints container info and per-stream info as JSON in one pass. Per ffprobe's own documentation, -print_format (or -of) selects the writer, -show_format prints the container section, and -show_streams prints one section per stream.
$path = '/var/www/storage/uploads/clip.mp4';
$cmd = sprintf(
'ffprobe -v quiet -print_format json -show_format -show_streams %s',
escapeshellarg($path)
);
$raw = shell_exec($cmd);
$meta = json_decode($raw, true);
if (!is_array($meta) || empty($meta['streams'])) {
throw new RuntimeException('ffprobe returned nothing usable for ' . $path);
}
escapeshellarg() is not optional. The filename arrives from a user in most PHP apps, and a file called clip.mp4; rm -rf . is a command, not a file. Note also that ffprobe must be on the PHP process's PATH, which is usually shorter than your shell's: if the command works as your user and returns empty from PHP-FPM, hardcode /usr/bin/ffprobe.
The visible failure mode is worth knowing. On a truncated upload, ffprobe exits non-zero and shell_exec() hands you an empty string, not an exception. Drop -v quiet while debugging and you'll see the real line, usually moov atom not found, which means the file arrived incomplete rather than corrupt.
Reading duration and dimensions with PHP-FFMpeg
PHP-FFMpeg wraps the same two binaries in an object API, and its FFMpeg\FFProbe class is what FFMpeg\FFMpeg uses internally. The project README documents duration as a format read and dimensions as a stream read.
$ffprobe = FFMpeg\FFProbe::create();
$duration = $ffprobe->format($path)->get('duration'); // "120.500000"
$video = $ffprobe->streams($path)->videos()->first();
$width = $video->getDimensions()->getWidth();
$height = $video->getDimensions()->getHeight();
$codec = $video->get('codec_name'); // "h264"
$ffprobe->isValid($path) gives you a boolean guard before any of that, which is handy in a Laravel form request. What the README does not tell you is what happens on a host without the binaries: PHP-FFMpeg throws Executable not found, proposed: avprobe, ffprobe out of FFMpeg/Driver/FFProbeDriver.php, and the documented workaround is passing explicit ffmpeg.binaries and ffprobe.binaries paths to a static build you uploaded yourself. That works until the host's open_basedir or noexec mount says otherwise. composer require never installs FFmpeg. It installs a wrapper around a binary you still have to own, patch and run, which is the same trap covered in PHP-FFMpeg vs the dead PECL extension.
Probing a video in PHP with no ffprobe installed
When exec, shell_exec and proc_open are all in disable_functions, the metadata has to come from somewhere that already has FFmpeg. FFmpeg Micro exposes probing as one authenticated call: POST /v1/ffprobe with a media_url, returning the same streams array and format object your parsing code already expects, documented on the FFprobe API reference.
$ch = curl_init('https://api.ffmpeg-micro.com/v1/ffprobe');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . getenv('FFMPEG_MICRO_KEY'),
],
CURLOPT_POSTFIELDS => json_encode([
'media_url' => 'https://cdn.example.com/uploads/clip.mp4',
]),
]);
$meta = json_decode(curl_exec($ch), true);
// $meta['format']['duration'], $meta['streams'][0]['width'], ...
media_url takes a public HTTPS URL, a gs:// path or S3, so a private upload needs to land in storage first: move_uploaded_file() to your bucket, or the presigned upload endpoint in the quickstart. There's a first-party PHP package on Packagist (ffmpeg-micro/ffmpeg-micro-php, PHP 8.1+) if you'd rather not hand-roll cURL. The free tier is 200 tokens with no card, one token buys 10 seconds of input metered per second, and failed jobs are never billed, so probing a library of short clips to backfill a duration column costs very little. Status checks, job listing and downloads aren't billed at all.
Honest boundary: if you already run FFmpeg on your own server for transcoding, don't add a network round trip just to read a header. Shell out. The API route earns its place when the binary isn't there, or when the person who has to patch it is you and nobody owns that Docker layer.
Which route fits which host
Pick by constraint, not by preference, because three of these four constraints are decided by your hosting plan rather than by you.
| Route | Needs | Gives you | Breaks when |
|---|---|---|---|
| `shell_exec` + ffprobe JSON | ffprobe binary, shell functions enabled | Full `format` and `streams`, side data included | `disable_functions`, `noexec`, slim images |
| PHP-FFMpeg `FFProbe` | Both binaries, Composer, `proc_open` | Typed objects, `isValid()` | Same hosts, with a longer stack trace |
| `POST /v1/ffprobe` | An API key and a reachable URL | Same JSON shape, no binary | The file can't be exposed to a URL at all |
| getID3 (pure PHP) | Composer only | `playtime_seconds`, `resolution_x/y` | 64-bit `mvhd` MP4s, partial MPEG-4 parsing |
Four mistakes that make this harder than it looks in PHP
Every one of these produces working-looking code that returns a wrong number on real files, which is worse than an exception.
Trusting format.duration and nothing else
format.duration is the container's length and the video stream's duration is that stream's length, and in an MP4 with audio the container figure usually tracks the audio, which often runs a fraction of a second longer than the picture. For a cost estimate or a "max 60 seconds" upload rule, format.duration is the right field. For frame math, read the video stream. Some containers, WebM and MKV especially, omit per-stream duration entirely, so write the fallback once:
$video = null;
foreach ($meta['streams'] as $stream) {
if ($stream['codec_type'] === 'video') { $video = $stream; break; }
}
$duration = isset($meta['format']['duration'])
? (float) $meta['format']['duration']
: (float) ($video['duration'] ?? 0);
Reading width and height without checking rotation
A portrait iPhone clip is stored as 1920x1080 with a rotation flag, so a Laravel validator that insists on vertical video rejects a file the phone shows as vertical. Old PHP scripts handled this by parsing a rotate tag, and that stopped working: ffprobe no longer prints the rotate pseudo-tag, a change that landed around October 2021 after working in release 4.4 (FFmpeg bug 9452), and rotation now ships as Display Matrix stream side data, which FFmpeg confirmed on ffmpeg-devel as the replacement for the deprecated metadata key. Display dimensions mean swapping the axes yourself:
$rotation = 0;
foreach ($video['side_data_list'] ?? [] as $side) {
if (isset($side['rotation'])) { $rotation = (int) $side['rotation']; }
}
[$w, $h] = [(int) $video['width'], (int) $video['height']];
if (abs($rotation) === 90 || abs($rotation) === 270) {
[$w, $h] = [$h, $w];
}
Passing $_FILES['tmp_name'] to a queue job
$_FILES['userfile']['tmp_name'] is the server-side temporary filename of the upload, and PHP's manual is explicit that the file is deleted from the temporary directory at the end of the request unless it has been moved or renamed. Dispatch a job carrying that path and the worker gets a path to nothing, usually minutes later and only under load. Call move_uploaded_file() first and queue the permanent path, or probe inside the request if the answer gates the response.
Reaching for ffmpeg_movie or getID3 without reading their fine print
Half the results for this question are documentation for the ffmpeg-php PECL extension, whose ffmpeg_movie::getDuration(), getFrameWidth() and getVideoCodec() methods people still copy. Its own project page dates the last release to March 2017 and targets PHP 4.3.0 and up, with no PHP 8 support, so pecl install ffmpeg on PHP 8.4 costs you an afternoon and returns nothing. getID3 is the genuine pure-PHP option and installs with Composer, deriving playtime_seconds from the container, but its QuickTime module reads mvhd at fixed 32-bit offsets and ignores the version field, so an MP4 with a version-1 64-bit mvhd is reported as corrupt with a wrong duration. Fine for a rough upload cap, not for anything you bill on.
FAQ about reading video metadata in PHP
Why does my portrait phone video report 1920x1080 in PHP?
A portrait recording from an iPhone or Android phone is encoded landscape with a Display Matrix that tells players to rotate it, so ffprobe reports the coded size of 1920x1080 and the player shows 1080x1920. Read side_data_list on the video stream, and swap width and height when rotation is 90, -90 or 270.
What do I do when PHP-FFMpeg throws "Executable not found, proposed: avprobe, ffprobe"?
That error means PHP-FFMpeg searched the process PATH and found neither binary, so either point it at an explicit path with the ffprobe.binaries config key, or stop depending on a local binary and send the file's URL to a hosted ffprobe call instead. On shared hosting the second option is usually the only one that survives, for the same reason shell_exec being disabled ends the shell-out route.
How do I reject HEVC uploads from PHP?
Read codec_name on the first video stream: HEVC files report hevc, H.264 files report h264. Check codec_tag_string too if Apple playback matters, since FFmpeg writes HEVC as hev1 by default and QuickTime only decodes hvc1.
Can I get duration without downloading the whole video?
ffprobe can read an HTTPS URL directly and will range-request only the header on a file written with -movflags +faststart, which puts the moov atom at the front. On a file whose moov sits at the end, the probe pulls the tail or the entire object, so a 2 GB master costs you 2 GB of transfer to learn its length.
Point your parsing code at whichever of the three routes your host allows, keep the duration fallback and the rotation swap in one helper, and the same JSON shape works across all three. The FFprobe API reference has the full response payload and the validate-then-transcode pattern it feeds. Sign up free and you get a key and 200 tokens on the free tier with no server to run and no FFmpeg to install, callable from your PHP code, from n8n, Make and Zapier, or from your AI agents.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Transcode H265 to H264 without installing a converter
Transcode H265 to H264 with one FFmpeg command or one API call: the CRF, preset and pixel format settings that platforms accept, and how to batch a folder.

FFmpeg JSON video composition shipped. Not in your build.
FFmpeg JSON video composition landed in git master as libplacebo_scene. What it needs, why your build returns nothing, and how to composite the same scene now.

Transcode every user uploaded video? Normalize to one house spec
Do you need to transcode user uploaded video? Probe with ffprobe first, normalize to one house spec, remux what already conforms, and re-encode only the rest.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free