phpffmpegapi

Get video duration and codec in PHP without ffprobe installed

··9 min read
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_streams and json_decode() the output, when you control the box and exec isn't in disable_functions.
  • Install PHP-FFMpeg with composer require php-ffmpeg/php-ffmpeg and read metadata off its FFProbe object, which is a nicer API over that same binary.
  • POST the file's URL to a hosted ffprobe endpoint and parse the identical streams array, 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.duration with the video stream as the tiebreaker, and check the video stream's side_data_list for rotation before you believe width and height.

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.

RouteNeedsGives youBreaks when
`shell_exec` + ffprobe JSONffprobe binary, shell functions enabledFull `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 URLSame JSON shape, no binaryThe 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.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Ready to process videos at scale?

Start using FFmpeg Micro's simple API today. No infrastructure required.

Get Started Free