mhor/php-mediainfo
A PHP wrapper around the MediaInfo CLI for reading detailed metadata from audio and video files. It provides a clean object-oriented API for codecs, duration, bitrate, resolution, channels, and track details, returning parsed output for easy inspection.
mhor/php-mediainfo is a thin object-oriented wrapper around the mediainfo binary. It does not parse media itself — it shells out, reads the XML, and hydrates typed containers. So step zero is installing the CLI:
# Debian/Ubuntu
apt-get install mediainfo
# macOS
brew install mediainfo
# verify it's on PATH for the PHP user (php-fpm, queue workers, cron)
mediainfo --Version
Then:
composer require mhor/php-mediainfo
Minimal first use case — you have an upload and need duration, resolution and codec:
use Mhor\MediaInfo\MediaInfo;
$mediaInfo = new MediaInfo();
$mediaInfo->setConfig('command', '/usr/bin/mediainfo'); // absolute path is safer than relying on PATH
$container = $mediaInfo->getInfo('/storage/app/videos/clip.mp4', true); // true = treat as local file
$general = $container->getGeneral();
$video = $container->getVideos()[0] ?? null;
$durationMs = $general->get('duration')?->getMilliseconds();
$width = $video?->get('width')?->getAbsoluteValue();
Where to look first, in order:
Mhor\MediaInfo\MediaInfo — config keys and the getInfo() entry point.Mhor\MediaInfo\Container\MediaInfoContainer — the getGeneral() / getVideos() / getAudios() / getSubtitles() accessors.Mhor\MediaInfo\Model\* (General, Video, Audio, Text, Image, Menu) — each is a keyed bag of attributes with get(), getAttributes().Mhor\MediaInfo\Attribute\* (Duration, Size, Ratio, Mode, Rate) — the typed value objects you'll actually be formatting.Wrap it in a single application service. Don't scatter new MediaInfo() through controllers. Bind a configured instance and expose a narrow API:
final class MediaProbe
{
public function __construct(private MediaInfo $mediaInfo) {}
public function probe(string $absolutePath): MediaMetadata
{
$c = $this->mediaInfo->getInfo($absolutePath, true);
// map container -> your own DTO, so the rest of the app
// never depends on the package's object graph
}
}
The DTO mapping is the single highest-value habit with this package: it lets you swap in ffprobe later without touching call sites, and it isolates the XML-version quirks below.
Treat tracks as plural. The package returns arrays because a single MKV or HLS master can carry many streams. Always design for getAudios() returning 0..n and pick deliberately:
$audios = $container->getAudios();
$primaryAudio = $audios[0] ?? null; // or filter by language / default flag
$hasSubtitles = count($container->getSubtitles()) > 0;
Probe once, persist. MediaInfo reads headers, not the whole file, so it's fast — but on a 5,000-item import it still means 5,000 proc_open calls. Store the extracted JSON on the model (media_metadata column) at upload time and read from there in views. For imports, probe inside a queued job, chunked.
Batch loops with guard clauses. A corrupt or zero-byte file won't necessarily throw — you get a container with an empty general track. Check before you index:
$general = $container->getGeneral();
if (!$container->getVideos() || !$general->get('duration')) {
throw new UnprocessableMediaException($path);
}
Testing. Unit tests should not require the binary. Either (a) build MediaInfoContainer/model objects directly from fixture arrays captured from real output, or (b) mock MediaInfo so getInfo() returns a hand-built container. Keep a small tests/fixtures/ set of real XML dumps (one MP4, one MKV with two audio tracks, one with subtitles) and assert your DTO mapping against them.
Formatting for UI. Attribute objects are inconsistent about output — some are __toString-able human strings, some need explicit getters. Normalise in one presenter:
$size = $general->get('file_size'); // Size
$human = (string) $size; // "1.32 GiB"-ish, MediaInfo-formatted
$bytes = $size->getAbsoluteValue();
The second argument to getInfo() is not optional in spirit. getInfo($path) defaults to treating the string as something other than a local file (URL/stream handling). Always pass true for a filesystem path, or you'll get confusing empty results on paths that clearly exist.
Old XML vs new XML output is the #1 breakage. MediaInfo changed its XML schema around v0.7.83+; field names, unit conventions (notably duration in ms vs seconds) and casing shifted. The wrapper exposes setConfig('use_oldxml_output', true|false) — set it explicitly to match the binary version your containers ship with, and pin the binary in your Docker base image. Never rely on "whatever apt gives us."
Key names are not always what you expect. Keys come straight from MediaInfo's output (bit_rate, frame_rate, format_profile, duration), and the same logical field can be named differently across track types. When something returns null, dump array_keys($track->getAttributes()) — that is the fastest debugging trick available here. get() returns null silently rather than throwing.
Binary/path failures surface as exceptions from the process layer. In php-fpm the PATH is often shorter than your shell's, so mediainfo: not found at runtime is a classic. Set command to an absolute path, and add a health check or a deploy smoke test that probes a bundled 1-second fixture file.
Attribute values vary in type. get('duration') gives a Duration object with getMilliseconds(); get('bit_rate') typically gives a scalar. Don't assume uniformity — wrap every read in a typed helper. Also beware Duration on old-XML output being an integer millisecond count while new-XML gives fractional seconds; assert this in your fixtures.
Large/complex sources are slow or sparse. Blu-ray structures, ISO images, HLS manifests and remote URLs are slower and often return partial data (menus, images, multiple video tracks). Set a sensible timeout on your queue job; the package doesn't expose a probe timeout, so bound it at the process/job level.
Extension points are thin. MediaInfoBuilder exists for constructing containers programmatically (useful in tests), but there's no plugin system for custom attribute parsers — unknown fields land as plain strings. If you need a typed field MediaInfo emits but the package doesn't model, read it as a string and cast in your own mapper.
Maintenance reality check. The package is mature but low-activity, MIT-licensed, with no recent releases. That's fine for a stable CLI wrapper, but pin the version in composer.json, keep your MediaProbe abstraction thin, and know that ffprobe with -print_format json is a drop-in escape hatch if you ever need to leave.
How can I help you explore Laravel packages today?