Every media file carries its own dossier — duration, codecs, dimensions, rotation, bitrate, cover art, even the GPS point your phone quietly wrote into it. Mediabunny reads it all by touching only the bytes that matter, so a 5 GB file answers as fast as a 5 MB one. Drop one in and watch the byte counter; nothing is ever uploaded.
Inspect a file above and these fill with real numbers from your own device — bytes read, percentage of the file touched, and time to the full report. No synthetic benchmark.
| Mediabunny | mediainfo.js | Server ffprobe | <video> element | |
|---|---|---|---|---|
| Where it runs | Your browser · zero WASM | Your browser · WASM | Your server | Your browser |
| Download to start | ~156 KB gz ¹ | ~2.6 MB wasm ² | — | 0 · built-in |
| Uploads your file? | No | No | Yes — the whole file | No |
| A 5 GB file | ms — reads only header bytes | OK — chunked reads | Upload first (~13 min ³) | Loads, but tells you little |
| What you get | Duration, codecs, fps + VFR, bitrate, rotation, HDR, tags, cover art, GPS check, truncation probe | Very deep field list | Everything ffprobe knows | Duration + dimensions only |
| Can-decode check | Yes — and it decodes a real frame to prove it | No | No | canPlayType() guesses |
| Cost | Free | Free | $ server time | Free |
¹ Full library, gzipped, before tree-shaking — same measurement as the Convert demo; a metadata-only import is far smaller. ² MediaInfoModule.wasm 0.3.7 as served by unpkg, 2,565,723 bytes, measured 2026-06-11. ³ Uploading 5 GB at 50 Mb/s — arithmetic, your connection may differ. Remaining cells are qualitative.
1import { Input, ALL_FORMATS, BlobSource } from 'mediabunny';2import { describeTrack } from './tracks';3import { tailProbe, privacyScan } from './analysis';4import { makePoster } from './poster';56// Inspect any media file — reading only the bytes it needs.7export async function inspect(file) {8 let bytesRead = 0; // the receipt — watch how little gets read9 const source = new BlobSource(file);10 source.onread = (start, end) => { bytesRead += end - start; };1112 const input = new Input({ source, formats: ALL_FORMATS });13 try {14 const tracks = await input.getTracks();15 const report = {16 format: (await input.getFormat()).name, // container17 mimeType: await input.getMimeType(), // full codec string18 duration: await input.computeDuration(), // seconds19 tags: await input.getMetadataTags(), // title, cover art…20 tracks: await Promise.all(tracks.map(describeTrack)),21 };22 report.integrity = await tailProbe(input, tracks, report.duration);23 report.poster = await makePoster(tracks); // decode before dispose24 report.privacy = privacyScan(report.tags); // GPS, device, dates25 report.bytesRead = bytesRead; // vs. file.size26 return report;27 } finally {28 input.dispose(); // frees demuxer state + pending reads29 }30}